diff --git a/CHANGES.md b/CHANGES.md index e181390..6ab0eea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +# 4.8.0 + +* Add support for Captions API [#267](https://github.com/opentok/OpenTok-Ruby-SDK/pull/267) + # 4.7.1 * Updates docs comments for `Broadcasts` and `Sip` [#266](https://github.com/opentok/OpenTok-Ruby-SDK/pull/266) diff --git a/lib/opentok/captions.rb b/lib/opentok/captions.rb new file mode 100644 index 0000000..9489214 --- /dev/null +++ b/lib/opentok/captions.rb @@ -0,0 +1,61 @@ +module OpenTok + # A class for working with OpenTok captions. + class Captions + # @private + def initialize(client) + @client = client + end + + # Starts live captions for the specified OpenTok session. + # See the {https://tokbox.com/developer/guides/live-captions/ OpenTok Live Captions developer guide}. + # + # @example + # opts = { "language_code" => "en-GB", + # "max_duration" => 5000, + # "partial_captions" => false, + # "status_callback_url" => status_callback_url + # } + # response = opentok.captions.start(session_id, token, opts) + # + # @param [String] session_id The session ID corresponding to the session for which captions will start. + # @param [String] token The token for the session ID with which the SIP user will use to connect. + # @param [Hash] options A hash defining options for the captions. For example: + # @option options [String] :language_code The BCP-47 code for a spoken language used on this call. + # The default value is "en-US". The following language codes are supported: + # - "en-AU" (English, Australia) + # - "en-GB" (Englsh, UK) + # - "es-US" (English, US) + # - "zh-CN” (Chinese, Simplified) + # - "fr-FR" (French) + # - "fr-CA" (French, Canadian) + # - "de-DE" (German) + # - "hi-IN" (Hindi, Indian) + # - "it-IT" (Italian) + # - "ja-JP" (Japanese) + # - "ko-KR" (Korean) + # - "pt-BR" (Portuguese, Brazilian) + # - "th-TH" (Thai) + # @option options [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. + # @option options [Boolean] :partial_captions Whether to enable this to faster captioning at the cost of some + # degree of inaccuracies. The default value is `true`. + # @option options [String] :status_callback_url A publicly reachable URL controlled by the customer and capable + # of generating the content to be rendered without user intervention. The minimum length of the URL is 15 + # characters and the maximum length is 2048 characters. + # For more information, see {https://tokbox.com/developer/guides/live-captions/#live-caption-status-updates Live Caption status updates}. + def start(session_id, token, options = {}) + @client.start_live_captions(session_id, token, options) + end + + # Starts live captions for the specified OpenTok session. + # See the {https://tokbox.com/developer/guides/live-captions/ OpenTok Live Captions developer guide}. + # + # @example + # response = opentok.captions.stop(captions_id) + # + # @param [String] captions_id The ID for the captions to be stopped (returned from the `start` request). + def stop(captions_id) + @client.stop_live_captions(captions_id) + end + end +end diff --git a/lib/opentok/client.rb b/lib/opentok/client.rb index 862a40a..3a99f20 100644 --- a/lib/opentok/client.rb +++ b/lib/opentok/client.rb @@ -62,6 +62,8 @@ def create_session(opts) raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end + # Archives methods + def start_archive(session_id, opts) opts.extend(HashExtensions) body = { "sessionId" => session_id }.merge(opts.camelize_keys!) @@ -215,463 +217,526 @@ def select_streams_for_archive(archive_id, opts) raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def forceDisconnect(session_id, connection_id) - response = self.class.delete("/v2/project/#{@api_key}/session/#{session_id}/connection/#{connection_id}", { + # Broadcasts methods + + def start_broadcast(session_id, opts) + opts.extend(HashExtensions) + body = { :sessionId => session_id }.merge(opts.camelize_keys!) + response = self.class.post("/v2/project/#{@api_key}/broadcast", { + :body => body.to_json, :headers => generate_headers("Content-Type" => "application/json") }) case response.code - when 204 + when 200 response when 400 - raise ArgumentError, "Force disconnect failed. Connection ID #{connection_id} or Session ID #{session_id} is invalid" + raise OpenTokBroadcastError, "The broadcast could not be started. The request was invalid or invalid layout options or exceeded the limit of five simultaneous RTMP streams." when 403 - raise OpenTokAuthenticationError, "You are not authorized to forceDisconnect, check your authentication credentials or token type is non-moderator" + raise OpenTokAuthenticationError, "Authentication failed while starting a broadcast. API Key: #{@api_key}" + when 409 + raise OpenTokBroadcastError, "The broadcast has already been started for this session." + when 500 + raise OpenTokError, "OpenTok server error." + else + raise OpenTokBroadcastError, "The broadcast could not be started" + end + rescue StandardError => e + raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" + end + + def get_broadcast(broadcast_id) + response = self.class.get("/v2/project/#{@api_key}/broadcast/#{broadcast_id}", { + :headers => generate_headers + }) + case response.code + when 200 + response + when 400 + raise OpenTokBroadcastError, "The request was invalid." + when 403 + raise OpenTokAuthenticationError, "Authentication failed while getting a broadcast. API Key: #{@api_key}" when 404 - raise OpenTokConnectionError, "The client specified by the connection ID: #{connection_id} is not connected to the session" + raise OpenTokBroadcastError, "No matching broadcast found (with the specified ID)" + when 500 + raise OpenTokError, "OpenTok server error." + else + raise OpenTokBroadcastError, "Could not fetch broadcast information." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def force_mute_stream(session_id, stream_id) - response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/stream/#{stream_id}/mute", { - :headers => generate_headers("Content-Type" => "application/json") + def stop_broadcast(broadcast_id) + response = self.class.post("/v2/project/#{@api_key}/broadcast/#{broadcast_id}/stop", { + :headers => generate_headers }) case response.code when 200 response when 400 - raise ArgumentError, "Force mute failed. Stream ID #{stream_id} or Session ID #{session_id} is invalid" + raise OpenTokBroadcastError, "The request was invalid." when 403 - raise OpenTokAuthenticationError, "Authentication failed. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed while stopping a broadcast. API Key: #{@api_key}" when 404 - raise OpenTokConnectionError, "Either Stream ID #{stream_id} or Session ID #{session_id} is invalid" + raise OpenTokBroadcastError, "No matching broadcast found (with the specified ID) or it is already stopped" + when 500 + raise OpenTokError, "OpenTok server error." + else + raise OpenTokBroadcastError, "The broadcast could not be stopped." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def force_mute_session(session_id, opts) + def list_broadcasts(offset, count, session_id) + query = Hash.new + query[:offset] = offset unless offset.nil? + query[:count] = count unless count.nil? + query[:sessionId] = session_id unless session_id.nil? + response = self.class.get("/v2/project/#{@api_key}/broadcast", { + :query => query.empty? ? nil : query, + :headers => generate_headers, + }) + case response.code + when 200 + response + when 403 + raise OpenTokAuthenticationError, + "Authentication failed while retrieving broadcasts. API Key: #{@api_key}" + else + raise OpenTokBroadcastError, "The broadcasts could not be retrieved." + end + rescue StandardError => e + raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" + end + + def layout_broadcast(broadcast_id, opts) opts.extend(HashExtensions) - body = opts.camelize_keys! - response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/mute", { - :body => body.to_json, + response = self.class.put("/v2/project/#{@api_key}/broadcast/#{broadcast_id}/layout", { + :body => opts.camelize_keys!.to_json, :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 200 response when 400 - raise ArgumentError, "Force mute failed. The request could not be processed due to a bad request" + raise OpenTokBroadcastError, "The layout operation could not be performed. The request was invalid or invalid layout options." when 403 - raise OpenTokAuthenticationError, "Authentication failed. API Key: #{@api_key}" - when 404 - raise OpenTokConnectionError, "Session ID #{session_id} is invalid" + raise OpenTokAuthenticationError, "Authentication failed for broadcast layout. API Key: #{@api_key}" + when 500 + raise OpenTokError, "OpenTok server error." + else + raise OpenTokBroadcastError, "The broadcast layout could not be performed." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def signal(session_id, connection_id, opts) + def select_streams_for_broadcast(broadcast_id, opts) opts.extend(HashExtensions) - connectionPath = connection_id.to_s.empty? ? "" : "/connection/#{connection_id}" - url = "/v2/project/#{@api_key}/session/#{session_id}#{connectionPath}/signal" - response = self.class.post(url, { - :body => opts.camelize_keys!.to_json, + body = opts.camelize_keys! + response = self.class.patch("/v2/project/#{@api_key}/broadcast/#{broadcast_id}/streams", { + :body => body.to_json, :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 204 response when 400 - raise ArgumentError, "One of the signal properties — data, type, sessionId or connectionId — is invalid." + raise OpenTokBroadcastError, "The request was invalid." when 403 - raise OpenTokAuthenticationError, "You are not authorized to send the signal. Check your authentication credentials." + raise OpenTokAuthenticationError, "Authentication failed. API Key: #{@api_key}" when 404 - raise OpenTokError, "The client specified by the connectionId property is not connected to the session." - when 413 - raise OpenTokError, "The type string exceeds the maximum length (128 bytes), or the data string exceeds the maximum size (8 kB)." + raise OpenTokBroadcastError, "No matching broadcast found with the specified ID: #{broadcast_id}" + when 405 + raise OpenTokBroadcastError, "The broadcast was started with streamMode set to 'auto', which does not support stream manipulation." + when 500 + raise OpenTokError, "OpenTok server error." else - raise OpenTokError, "The signal could not be send." + raise OpenTokBroadcastError, "The broadcast streams could not be updated." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def connect_websocket(session_id, token, websocket_uri, opts) - opts.extend(HashExtensions) + # Captions methods + + def start_live_captions(session_id, token, options) + options.extend(HashExtensions) body = { "sessionId" => session_id, "token" => token, - "websocket" => { "uri" => websocket_uri }.merge(opts.camelize_keys!) - } + }.merge(options.camelize_keys!) - response = self.class.post("/v2/project/#{@api_key}/connect", { + response = self.class.post("/v2/project/#{@api_key}/captions", { :body => body.to_json, :headers => generate_headers("Content-Type" => "application/json") }) case response.code - when 200 + when 202 response when 400 - raise ArgumentError, "One of the properties is invalid." + raise OpenTokCaptionsError, "The request was invalid." when 403 - raise OpenTokAuthenticationError, "You are not authorized to start the call, check your authentication information." + raise OpenTokAuthenticationError, "Authentication failed while starting captions. API Key: #{@api_key}" when 409 - raise OpenTokWebSocketError, "Conflict. Only routed sessions are allowed to initiate Connect Calls." + raise OpenTokCaptionsError, "Live captions have already started for this OpenTok Session: #{session_id}" when 500 raise OpenTokError, "OpenTok server error." else - raise OpenTokWebSocketError, "The WebSocket could not be connected" + raise OpenTokCaptionsError, "Captions could not be started" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def dial(session_id, token, sip_uri, opts) - opts.extend(HashExtensions) - body = { "sessionId" => session_id, - "token" => token, - "sip" => { "uri" => sip_uri }.merge(opts.camelize_keys!) - } - - response = self.class.post("/v2/project/#{@api_key}/dial", { - :body => body.to_json, + def stop_live_captions(captions_id) + response = self.class.post("/v2/project/#{@api_key}/captions/#{captions_id}/stop", { :headers => generate_headers("Content-Type" => "application/json") }) case response.code - when 200 + when 202 response when 403 - raise OpenTokAuthenticationError, "Authentication failed while dialing a SIP session. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed while starting captions. API Key: #{@api_key}" when 404 - raise OpenTokSipError, "The SIP session could not be dialed. The Session ID does not exist: #{session_id}" + raise OpenTokCaptionsError, "No matching captions_id was found: #{captions_id}" + when 500 + raise OpenTokError, "OpenTok server error." else - raise OpenTokSipError, "The SIP session could not be dialed" + raise OpenTokCaptionsError, "Captions could not be stopped" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def play_dtmf_to_connection(session_id, connection_id, dtmf_digits) - body = { "digits" => dtmf_digits } + # Connections methods - response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/connection/#{connection_id}/play-dtmf", { - :body => body.to_json, - :headers => generate_headers("Content-Type" => "application/json") + def forceDisconnect(session_id, connection_id) + response = self.class.delete("/v2/project/#{@api_key}/session/#{session_id}/connection/#{connection_id}", { + :headers => generate_headers("Content-Type" => "application/json") }) case response.code - when 200 + when 204 response when 400 - raise ArgumentError, "One of the properties — dtmf_digits #{dtmf_digits} or session_id #{session_id} — is invalid." + raise ArgumentError, "Force disconnect failed. Connection ID #{connection_id} or Session ID #{session_id} is invalid" when 403 - raise OpenTokAuthenticationError, "Authentication failed. This can occur if you use an invalid OpenTok API key or an invalid JSON web token. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "You are not authorized to forceDisconnect, check your authentication credentials or token type is non-moderator" when 404 - raise OpenTokError, "The specified session #{session_id} does not exist or the client specified by the #{connection_id} property is not connected to the session." - else - raise OpenTokError, "An error occurred when attempting to play DTMF digits to the session" + raise OpenTokConnectionError, "The client specified by the connection ID: #{connection_id} is not connected to the session" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def play_dtmf_to_session(session_id, dtmf_digits) - body = { "digits" => dtmf_digits } + # Renders methods - response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/play-dtmf", { - :body => body.to_json, - :headers => generate_headers("Content-Type" => "application/json") + def start_render(session_id, opts) + opts.extend(HashExtensions) + body = { :sessionId => session_id }.merge(opts.camelize_keys!) + response = self.class.post("/v2/project/#{@api_key}/render", { + :body => body.to_json, + :headers => generate_headers("Content-Type" => "application/json") }) case response.code - when 200 + when 202 response when 400 - raise ArgumentError, "One of the properties — dtmf_digits #{dtmf_digits} or session_id #{session_id} — is invalid." + raise OpenTokRenderError, "The render could not be started. The request was invalid." when 403 - raise OpenTokAuthenticationError, "Authentication failed. This can occur if you use an invalid OpenTok API key or an invalid JSON web token. API Key: #{@api_key}" - when 404 - raise OpenTokError, "The specified session does not exist. Session ID: #{session_id}" + raise OpenTokAuthenticationError, "Authentication failed while starting a render. API Key: #{@api_key}" + when 500 + raise OpenTokError, "OpenTok server error." else - raise OpenTokError, "An error occurred when attempting to play DTMF digits to the session" + raise OpenTokRenderError, "The render could not be started" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def info_stream(session_id, stream_id) - streamId = stream_id.to_s.empty? ? '' : "/#{stream_id}" - url = "/v2/project/#{@api_key}/session/#{session_id}/stream#{streamId}" - response = self.class.get(url, - headers: generate_headers('Content-Type' => 'application/json')) + def get_render(render_id) + response = self.class.get("/v2/project/#{@api_key}/render/#{render_id}", { + :headers => generate_headers + }) case response.code when 200 response when 400 - raise ArgumentError, 'Invalid request. You did not pass in a valid session ID or stream ID.' + raise OpenTokRenderError, "The request was invalid." when 403 - raise OpenTokAuthenticationError, 'Check your authentication credentials. You passed in an invalid OpenTok API key.' - when 408 - raise ArgumentError, 'You passed in an invalid stream ID.' + raise OpenTokAuthenticationError, "Authentication failed while getting a render. API Key: #{@api_key}" + when 404 + raise OpenTokRenderError, "No matching render found (with the specified ID)" when 500 - raise OpenTokError, 'OpenTok server error.' + raise OpenTokError, "OpenTok server error." else - raise OpenTokError, 'Could not fetch the stream information.' + raise OpenTokRenderError, "Could not fetch render information." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def layout_streams(session_id, opts) - opts.extend(HashExtensions) - response = self.class.put("/v2/project/#{@api_key}/session/#{session_id}/stream", { - :body => opts.camelize_keys!.to_json, - :headers => generate_headers("Content-Type" => "application/json") + def stop_render(render_id) + response = self.class.delete("/v2/project/#{@api_key}/render/#{render_id}", { + :headers => generate_headers }) case response.code - when 200 + when 204 response when 400 - raise OpenTokStreamLayoutError, "Setting the layout failed. The request was invalid or invalid layout options were given." + raise OpenTokRenderError, "The request was invalid." when 403 - raise OpenTokAuthenticationError, "Authentication failed. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed while stopping a render. API Key: #{@api_key}" + when 404 + raise OpenTokRenderError, "No matching render found (with the specified ID) or it is already stopped" when 500 - raise OpenTokError, "Setting the layout failed. OpenTok server error." + raise OpenTokError, "OpenTok server error." else - raise OpenTokStreamLayoutError, "Setting the layout failed." + raise OpenTokRenderError, "The render could not be stopped." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def start_broadcast(session_id, opts) - opts.extend(HashExtensions) - body = { :sessionId => session_id }.merge(opts.camelize_keys!) - response = self.class.post("/v2/project/#{@api_key}/broadcast", { - :body => body.to_json, - :headers => generate_headers("Content-Type" => "application/json") + def list_renders(offset, count) + query = Hash.new + query[:offset] = offset unless offset.nil? + query[:count] = count unless count.nil? + response = self.class.get("/v2/project/#{@api_key}/render", { + :query => query.empty? ? nil : query, + :headers => generate_headers, }) case response.code when 200 response - when 400 - raise OpenTokBroadcastError, "The broadcast could not be started. The request was invalid or invalid layout options or exceeded the limit of five simultaneous RTMP streams." when 403 - raise OpenTokAuthenticationError, "Authentication failed while starting a broadcast. API Key: #{@api_key}" - when 409 - raise OpenTokBroadcastError, "The broadcast has already been started for this session." + raise OpenTokAuthenticationError, + "Authentication failed while retrieving renders. API Key: #{@api_key}" when 500 raise OpenTokError, "OpenTok server error." else - raise OpenTokBroadcastError, "The broadcast could not be started" + raise OpenTokRenderError, "The renders could not be retrieved." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def get_broadcast(broadcast_id) - response = self.class.get("/v2/project/#{@api_key}/broadcast/#{broadcast_id}", { - :headers => generate_headers + # Sip methods + + def dial(session_id, token, sip_uri, opts) + opts.extend(HashExtensions) + body = { "sessionId" => session_id, + "token" => token, + "sip" => { "uri" => sip_uri }.merge(opts.camelize_keys!) + } + + response = self.class.post("/v2/project/#{@api_key}/dial", { + :body => body.to_json, + :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 200 response - when 400 - raise OpenTokBroadcastError, "The request was invalid." when 403 - raise OpenTokAuthenticationError, "Authentication failed while getting a broadcast. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed while dialing a SIP session. API Key: #{@api_key}" when 404 - raise OpenTokBroadcastError, "No matching broadcast found (with the specified ID)" - when 500 - raise OpenTokError, "OpenTok server error." + raise OpenTokSipError, "The SIP session could not be dialed. The Session ID does not exist: #{session_id}" else - raise OpenTokBroadcastError, "Could not fetch broadcast information." + raise OpenTokSipError, "The SIP session could not be dialed" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def stop_broadcast(broadcast_id) - response = self.class.post("/v2/project/#{@api_key}/broadcast/#{broadcast_id}/stop", { - :headers => generate_headers + def play_dtmf_to_connection(session_id, connection_id, dtmf_digits) + body = { "digits" => dtmf_digits } + + response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/connection/#{connection_id}/play-dtmf", { + :body => body.to_json, + :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 200 response when 400 - raise OpenTokBroadcastError, "The request was invalid." + raise ArgumentError, "One of the properties — dtmf_digits #{dtmf_digits} or session_id #{session_id} — is invalid." when 403 - raise OpenTokAuthenticationError, "Authentication failed while stopping a broadcast. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed. This can occur if you use an invalid OpenTok API key or an invalid JSON web token. API Key: #{@api_key}" when 404 - raise OpenTokBroadcastError, "No matching broadcast found (with the specified ID) or it is already stopped" - when 500 - raise OpenTokError, "OpenTok server error." + raise OpenTokError, "The specified session #{session_id} does not exist or the client specified by the #{connection_id} property is not connected to the session." else - raise OpenTokBroadcastError, "The broadcast could not be stopped." + raise OpenTokError, "An error occurred when attempting to play DTMF digits to the session" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def list_broadcasts(offset, count, session_id) - query = Hash.new - query[:offset] = offset unless offset.nil? - query[:count] = count unless count.nil? - query[:sessionId] = session_id unless session_id.nil? - response = self.class.get("/v2/project/#{@api_key}/broadcast", { - :query => query.empty? ? nil : query, - :headers => generate_headers, + def play_dtmf_to_session(session_id, dtmf_digits) + body = { "digits" => dtmf_digits } + + response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/play-dtmf", { + :body => body.to_json, + :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 200 response + when 400 + raise ArgumentError, "One of the properties — dtmf_digits #{dtmf_digits} or session_id #{session_id} — is invalid." when 403 - raise OpenTokAuthenticationError, - "Authentication failed while retrieving broadcasts. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed. This can occur if you use an invalid OpenTok API key or an invalid JSON web token. API Key: #{@api_key}" + when 404 + raise OpenTokError, "The specified session does not exist. Session ID: #{session_id}" else - raise OpenTokBroadcastError, "The broadcasts could not be retrieved." + raise OpenTokError, "An error occurred when attempting to play DTMF digits to the session" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def layout_broadcast(broadcast_id, opts) - opts.extend(HashExtensions) - response = self.class.put("/v2/project/#{@api_key}/broadcast/#{broadcast_id}/layout", { - :body => opts.camelize_keys!.to_json, - :headers => generate_headers("Content-Type" => "application/json") - }) + # Streams methods + + def info_stream(session_id, stream_id) + streamId = stream_id.to_s.empty? ? '' : "/#{stream_id}" + url = "/v2/project/#{@api_key}/session/#{session_id}/stream#{streamId}" + response = self.class.get(url, + headers: generate_headers('Content-Type' => 'application/json')) case response.code when 200 response when 400 - raise OpenTokBroadcastError, "The layout operation could not be performed. The request was invalid or invalid layout options." + raise ArgumentError, 'Invalid request. You did not pass in a valid session ID or stream ID.' when 403 - raise OpenTokAuthenticationError, "Authentication failed for broadcast layout. API Key: #{@api_key}" + raise OpenTokAuthenticationError, 'Check your authentication credentials. You passed in an invalid OpenTok API key.' + when 408 + raise ArgumentError, 'You passed in an invalid stream ID.' when 500 - raise OpenTokError, "OpenTok server error." + raise OpenTokError, 'OpenTok server error.' else - raise OpenTokBroadcastError, "The broadcast layout could not be performed." + raise OpenTokError, 'Could not fetch the stream information.' end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def select_streams_for_broadcast(broadcast_id, opts) + def layout_streams(session_id, opts) opts.extend(HashExtensions) - body = opts.camelize_keys! - response = self.class.patch("/v2/project/#{@api_key}/broadcast/#{broadcast_id}/streams", { - :body => body.to_json, + response = self.class.put("/v2/project/#{@api_key}/session/#{session_id}/stream", { + :body => opts.camelize_keys!.to_json, :headers => generate_headers("Content-Type" => "application/json") }) case response.code - when 204 + when 200 response when 400 - raise OpenTokBroadcastError, "The request was invalid." + raise OpenTokStreamLayoutError, "Setting the layout failed. The request was invalid or invalid layout options were given." when 403 raise OpenTokAuthenticationError, "Authentication failed. API Key: #{@api_key}" - when 404 - raise OpenTokBroadcastError, "No matching broadcast found with the specified ID: #{broadcast_id}" - when 405 - raise OpenTokBroadcastError, "The broadcast was started with streamMode set to 'auto', which does not support stream manipulation." when 500 - raise OpenTokError, "OpenTok server error." + raise OpenTokError, "Setting the layout failed. OpenTok server error." else - raise OpenTokBroadcastError, "The broadcast streams could not be updated." + raise OpenTokStreamLayoutError, "Setting the layout failed." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def start_render(session_id, opts) - opts.extend(HashExtensions) - body = { :sessionId => session_id }.merge(opts.camelize_keys!) - response = self.class.post("/v2/project/#{@api_key}/render", { - :body => body.to_json, + def force_mute_stream(session_id, stream_id) + response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/stream/#{stream_id}/mute", { :headers => generate_headers("Content-Type" => "application/json") }) case response.code - when 202 + when 200 response when 400 - raise OpenTokRenderError, "The render could not be started. The request was invalid." + raise ArgumentError, "Force mute failed. Stream ID #{stream_id} or Session ID #{session_id} is invalid" when 403 - raise OpenTokAuthenticationError, "Authentication failed while starting a render. API Key: #{@api_key}" - when 500 - raise OpenTokError, "OpenTok server error." - else - raise OpenTokRenderError, "The render could not be started" + raise OpenTokAuthenticationError, "Authentication failed. API Key: #{@api_key}" + when 404 + raise OpenTokConnectionError, "Either Stream ID #{stream_id} or Session ID #{session_id} is invalid" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def get_render(render_id) - response = self.class.get("/v2/project/#{@api_key}/render/#{render_id}", { - :headers => generate_headers + def force_mute_session(session_id, opts) + opts.extend(HashExtensions) + body = opts.camelize_keys! + response = self.class.post("/v2/project/#{@api_key}/session/#{session_id}/mute", { + :body => body.to_json, + :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 200 response when 400 - raise OpenTokRenderError, "The request was invalid." + raise ArgumentError, "Force mute failed. The request could not be processed due to a bad request" when 403 - raise OpenTokAuthenticationError, "Authentication failed while getting a render. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed. API Key: #{@api_key}" when 404 - raise OpenTokRenderError, "No matching render found (with the specified ID)" - when 500 - raise OpenTokError, "OpenTok server error." - else - raise OpenTokRenderError, "Could not fetch render information." + raise OpenTokConnectionError, "Session ID #{session_id} is invalid" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def stop_render(render_id) - response = self.class.delete("/v2/project/#{@api_key}/render/#{render_id}", { - :headers => generate_headers + # Signals methods + + def signal(session_id, connection_id, opts) + opts.extend(HashExtensions) + connectionPath = connection_id.to_s.empty? ? "" : "/connection/#{connection_id}" + url = "/v2/project/#{@api_key}/session/#{session_id}#{connectionPath}/signal" + response = self.class.post(url, { + :body => opts.camelize_keys!.to_json, + :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 204 response when 400 - raise OpenTokRenderError, "The request was invalid." + raise ArgumentError, "One of the signal properties — data, type, sessionId or connectionId — is invalid." when 403 - raise OpenTokAuthenticationError, "Authentication failed while stopping a render. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "You are not authorized to send the signal. Check your authentication credentials." when 404 - raise OpenTokRenderError, "No matching render found (with the specified ID) or it is already stopped" - when 500 - raise OpenTokError, "OpenTok server error." + raise OpenTokError, "The client specified by the connectionId property is not connected to the session." + when 413 + raise OpenTokError, "The type string exceeds the maximum length (128 bytes), or the data string exceeds the maximum size (8 kB)." else - raise OpenTokRenderError, "The render could not be stopped." + raise OpenTokError, "The signal could not be send." end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - def list_renders(offset, count) - query = Hash.new - query[:offset] = offset unless offset.nil? - query[:count] = count unless count.nil? - response = self.class.get("/v2/project/#{@api_key}/render", { - :query => query.empty? ? nil : query, - :headers => generate_headers, + # WebSocket methods + + def connect_websocket(session_id, token, websocket_uri, opts) + opts.extend(HashExtensions) + body = { "sessionId" => session_id, + "token" => token, + "websocket" => { "uri" => websocket_uri }.merge(opts.camelize_keys!) + } + + response = self.class.post("/v2/project/#{@api_key}/connect", { + :body => body.to_json, + :headers => generate_headers("Content-Type" => "application/json") }) case response.code when 200 response + when 400 + raise ArgumentError, "One of the properties is invalid." when 403 - raise OpenTokAuthenticationError, - "Authentication failed while retrieving renders. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "You are not authorized to start the call, check your authentication information." + when 409 + raise OpenTokWebSocketError, "Conflict. Only routed sessions are allowed to initiate Connect Calls." when 500 raise OpenTokError, "OpenTok server error." else - raise OpenTokRenderError, "The renders could not be retrieved." + raise OpenTokWebSocketError, "The WebSocket could not be connected" end rescue StandardError => e raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end - end end diff --git a/lib/opentok/exceptions.rb b/lib/opentok/exceptions.rb index 0986f98..5bb6637 100644 --- a/lib/opentok/exceptions.rb +++ b/lib/opentok/exceptions.rb @@ -18,4 +18,6 @@ class OpenTokBroadcastError < OpenTokError; end class OpenTokWebSocketError < OpenTokError; end # Defines errors raised when you perform Experience Composer render operations. class OpenTokRenderError < OpenTokError; end + # Defines errors raised when you perform Captions operations. + class OpenTokCaptionsError < OpenTokError; end end diff --git a/lib/opentok/opentok.rb b/lib/opentok/opentok.rb index b7ecfad..d47e5aa 100644 --- a/lib/opentok/opentok.rb +++ b/lib/opentok/opentok.rb @@ -12,6 +12,7 @@ require "opentok/signals" require "opentok/broadcasts" require "opentok/renders" +require "opentok/captions" module OpenTok # Contains methods for creating OpenTok sessions and generating tokens. It also includes @@ -214,6 +215,16 @@ def broadcasts @broadcasts ||= Broadcasts.new client end + # A Captions object, which lets you start and stop live captions for an OpenTok session. + def captions + @captions ||= Captions.new client + end + + # A Connections object, which lets you disconnect clients from an OpenTok session. + def connections + @connections ||= Connections.new client + end + # A Renders object, which lets you work with OpenTok Experience Composer renders. def renders @renders ||= Renders.new client @@ -234,11 +245,6 @@ def signals @signals ||= Signals.new client end - # A Connections object, which lets you disconnect clients from an OpenTok session. - def connections - @connections ||= Connections.new client - end - # A WebSocket object, which lets you connect OpenTok streams to a WebSocket URI. def websocket @websocket ||= WebSocket.new client diff --git a/lib/opentok/version.rb b/lib/opentok/version.rb index faf23df..21e8329 100644 --- a/lib/opentok/version.rb +++ b/lib/opentok/version.rb @@ -1,4 +1,4 @@ module OpenTok # @private - VERSION = '4.7.1' + VERSION = '4.8.0' end diff --git a/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_starting_captions.yml b/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_starting_captions.yml new file mode 100644 index 0000000..6b0c0d3 --- /dev/null +++ b/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_starting_captions.yml @@ -0,0 +1,44 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/captions + body: + encoding: UTF-8 + string: '{"sessionId":"SESSIONID","token":"TOKENID"}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJpc3QiOiJwcm9qZWN0IiwiYWxnIjoiSFMyNTYifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.BplMVhJWx4ld7KLKXqEmow6MjNPPFw9W8IHCMfeb120 + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 202 + message: Accepted + headers: + Date: + - Tue, 05 Sep 2023 11:32:49 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Set-Cookie: + - AWSALBTG=V0iIksb77idvUqENJbP7kHbxorCidH5rXzTdrmAq244qBgmkoAZJ+QZE0gPn/fKjmIEtO29S43zhxzH3X5Ao8Nf/8KMvioOduANad0cg6/rMMbSKhWdxNJkcePyrYLO/6voP37Lk8LamlXqv3QK6lj9SCvA+/wyUB6CV+i1JY8XSc1ba/+s=; + Expires=Tue, 12 Sep 2023 11:32:49 GMT; Path=/ + - AWSALBTGCORS=V0iIksb77idvUqENJbP7kHbxorCidH5rXzTdrmAq244qBgmkoAZJ+QZE0gPn/fKjmIEtO29S43zhxzH3X5Ao8Nf/8KMvioOduANad0cg6/rMMbSKhWdxNJkcePyrYLO/6voP37Lk8LamlXqv3QK6lj9SCvA+/wyUB6CV+i1JY8XSc1ba/+s=; + Expires=Tue, 12 Sep 2023 11:32:49 GMT; Path=/; SameSite=None; Secure + X-Opentok-Trace: + - f=unknown&s=cerberus&u=B8FAC1E2-3DCE-40A9-9CC3-866750E7C7A5 + body: + encoding: UTF-8 + string: '{ "captionsId": "7c0680fc-6274-4de5-a66f-d0648e8d3ac2" }' + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 6.0.0 diff --git a/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_starting_captions_with_options.yml b/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_starting_captions_with_options.yml new file mode 100644 index 0000000..f7c8680 --- /dev/null +++ b/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_starting_captions_with_options.yml @@ -0,0 +1,44 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/captions + body: + encoding: UTF-8 + string: '{"sessionId":"SESSIONID","token":"TOKENID","languageCode":"en-GB","maxDuration":5000,"partialCaptions":false,"statusCallbackUrl":"https://example.com/captions/status"}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJpc3QiOiJwcm9qZWN0IiwiYWxnIjoiSFMyNTYifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.BplMVhJWx4ld7KLKXqEmow6MjNPPFw9W8IHCMfeb120 + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 202 + message: Accepted + headers: + Date: + - Tue, 05 Sep 2023 12:05:23 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Set-Cookie: + - AWSALBTG=Qam0XgGzqhEaBC7TB9UprWCTZ6omkQZDorQLpkU8Kb3+iSIJsINH6Rh13nWnaLryAUc08wZEEc0VAi6ZXVa5gVLE/5YeIvMT9WP8VzJLFyvQ/bMe7TIDvp/QronHO4tohNGpvrFQxCC5LElWX5JnN3sCr5WpUHqEPQr0jz9TtUmKJYIh0So=; + Expires=Tue, 12 Sep 2023 12:05:23 GMT; Path=/ + - AWSALBTGCORS=Qam0XgGzqhEaBC7TB9UprWCTZ6omkQZDorQLpkU8Kb3+iSIJsINH6Rh13nWnaLryAUc08wZEEc0VAi6ZXVa5gVLE/5YeIvMT9WP8VzJLFyvQ/bMe7TIDvp/QronHO4tohNGpvrFQxCC5LElWX5JnN3sCr5WpUHqEPQr0jz9TtUmKJYIh0So=; + Expires=Tue, 12 Sep 2023 12:05:23 GMT; Path=/; SameSite=None; Secure + X-Opentok-Trace: + - f=unknown&s=cerberus&u=5DB017B0-6FC7-444F-BDE9-20D3CBA7B2EA + body: + encoding: UTF-8 + string: '{ "captionsId": "7c0680fc-6274-4de5-a66f-d0648e8d3ac2" }' + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 6.0.0 diff --git a/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_stopping_captions.yml b/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_stopping_captions.yml new file mode 100644 index 0000000..e52e9fe --- /dev/null +++ b/spec/cassettes/OpenTok_Captions/receives_a_valid_response_when_stopping_captions.yml @@ -0,0 +1,44 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/captions/CAPTIONSID/stop + body: + encoding: UTF-8 + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJpc3QiOiJwcm9qZWN0IiwiYWxnIjoiSFMyNTYifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.BplMVhJWx4ld7KLKXqEmow6MjNPPFw9W8IHCMfeb120 + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 202 + message: Accepted + headers: + Date: + - Tue, 05 Sep 2023 11:30:55 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Set-Cookie: + - AWSALBTG=LJIybl0y9LXphBJqNdoYf94M2EQTLnT/IhYp4UD+lzvSSZfKfdshtXSXDJ+gplgMuZ+3zg7mtnQoUF7ZhM2IRiL6VYJq88ltUPP075OdNgDwWRpYLq+doFLXeee4skbljcUpgTAWkCp1MReHhYdiTO8G36cgW9hPyWE9/jIMcfhN6HzVv5Q=; + Expires=Tue, 12 Sep 2023 11:30:55 GMT; Path=/ + - AWSALBTGCORS=LJIybl0y9LXphBJqNdoYf94M2EQTLnT/IhYp4UD+lzvSSZfKfdshtXSXDJ+gplgMuZ+3zg7mtnQoUF7ZhM2IRiL6VYJq88ltUPP075OdNgDwWRpYLq+doFLXeee4skbljcUpgTAWkCp1MReHhYdiTO8G36cgW9hPyWE9/jIMcfhN6HzVv5Q=; + Expires=Tue, 12 Sep 2023 11:30:55 GMT; Path=/; SameSite=None; Secure + X-Opentok-Trace: + - f=unknown&s=cerberus&u=22ECEF08-1EBD-4554-BF7B-04857DE8616A + body: + encoding: UTF-8 + string: '' + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 6.0.0 diff --git a/spec/opentok/captions_spec.rb b/spec/opentok/captions_spec.rb new file mode 100644 index 0000000..1ae4e9a --- /dev/null +++ b/spec/opentok/captions_spec.rb @@ -0,0 +1,44 @@ +require "opentok/opentok" +require "opentok/captions" +require "opentok/version" +require "spec_helper" + +describe OpenTok::Captions do + before(:each) do + now = Time.parse("2017-04-18 20:17:40 +1000") + allow(Time).to receive(:now) { now } + end + + let(:api_key) { "123456" } + let(:api_secret) { "1234567890abcdef1234567890abcdef1234567890" } + let(:session_id) { "SESSIONID" } + let(:captions_id) { "CAPTIONSID" } + let(:expiring_token) { "TOKENID" } + let(:status_callback_url) { "https://example.com/captions/status" } + let(:opentok) { OpenTok::OpenTok.new api_key, api_secret } + let(:captions) { opentok.captions } + subject { captions } + + it "receives a valid response when starting captions", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + response = captions.start(session_id, expiring_token) + expect(response).not_to be_nil + expect(response.code).to eq(202) + end + + it "receives a valid response when starting captions with options", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + opts = { "language_code" => "en-GB", + "max_duration" => 5000, + "partial_captions" => false, + "status_callback_url" => status_callback_url + } + + response = captions.start(session_id, expiring_token, opts) + expect(response).not_to be_nil + expect(response.code).to eq(202) + end + + it "receives a valid response when stopping captions", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + response = captions.stop(captions_id) + expect(response.code).to eq(202) + end +end