diff --git a/README.md b/README.md index ec7ffa83..e499c138 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,14 @@ [![Build Status](https://travis-ci.org/opentok/OpenTok-Ruby-SDK.png)](https://travis-ci.org/opentok/OpenTok-Ruby-SDK) The OpenTok Ruby SDK lets you generate -[sessions](http://www.tokbox.com/opentok/tutorials/create-session/) and -[tokens](http://www.tokbox.com/opentok/tutorials/create-token/) for -[OpenTok](http://www.tokbox.com/) applications, and -[archive](https://tokbox.com/opentok/tutorials/archiving) OpenTok sessions. +[sessions](https://tokbox.com/developer/guides/create-session/) and +[tokens](https://tokbox.com/developer/guides/create-token/) for +[OpenTok](http://www.tokbox.com/) applications. It also includes methods for +working with OpenTok [archives](https://tokbox.com/developer/guides/archiving), +working with OpenTok [live streaming +broadcasts](https://tokbox.com/developer/guides/broadcast/live-streaming/), +working with OpenTok [SIP interconnect](https://tokbox.com/developer/guides/sip), +and [disconnecting clients from sessions](https://tokbox.com/developer/guides/moderation/rest/). # Installation @@ -17,7 +21,7 @@ Bundler helps manage dependencies for Ruby projects. Find more info here: 3.0.3" +gem "opentok", "~> 3.1.0" ``` Allow bundler to install the change. @@ -47,11 +51,19 @@ opentok = OpenTok::OpenTok.new api_key, api_secret ## Creating Sessions -To create an OpenTok Session, use the `OpenTok#create_session(properties)` method. The -`properties` parameter is an optional Hash used to specify whether you are creating a session that -uses the OpenTok Media Server and specifying a location hint. The `session_id` method of the -returned `OpenTok::Session` instance is useful to get a sessionId that can be saved to a persistent -store (e.g. database). +To create an OpenTok Session, use the `OpenTok#create_session(properties)` method. +The `properties` parameter is an optional Hash used to specify the following: + +* Whether the session uses the [OpenTok Media + Router](https://tokbox.com/developer/guides/create-session/#media-mode), + which is required for some OpenTok features (such as archiving) + +* A location hint for the OpenTok server. + +* Whether the session is automatically archived. + +The `session_id` method of the returned `OpenTok::Session` instance is useful to +get a sessionId that can be saved to a persistent store (such as a database). ```ruby # Create a session that will attempt to transmit streams directly between clients. @@ -96,8 +108,37 @@ token = session.generate_token({ }); ``` +## Working with Streams + +Use this method to get information for an OpenTok stream or for all streams in a session. +For example, you can call this method to get information about layout classes used by an +OpenTok stream. + +To get information of a specific stream in a session, call +`opentok.streams.find(session_id, stream_id)`. The return object is a `Stream` object and +you can access various stream properties as shown in the following example (using RSpec notations): + +```ruby +expect(stream).to be_an_instance_of OpenTok::Stream +expect(stream.videoType).to eq 'camera' +expect(stream.layoutClassList.count).to eq 1 +expect(stream.layoutClassList.first).to eq "full" +``` + +To get information on all streams in a session, call `opentok.streams.all(session_id)`. +The return value is a `StreamList` object: + +```ruby +expect(all_streams).to be_an_instance_of OpenTok::StreamList +expect(all_streams.total).to eq 2 +expect(all_streams[0].layoutClassList[1]).to eq "focus" +``` + ## Working with Archives +You can only archive sessions that use the OpenTok Media Router +(sessions with the media mode set to routed). + You can start the recording of an OpenTok Session using the `opentok.archives.create(session_id, options)` method. This will return an `OpenTok::Archive` instance. The parameter `options` is an optional Hash used to set the `has_audio`, `has_video`, and `name` options. Note that you can @@ -127,6 +168,19 @@ archive = opentok.archives.create session_id :output_mode => :individual The `:output_mode => :composed` setting (the default) causes all streams in the archive to be recorded to a single (composed) file. +For composed archives you can set the resolution of the archive, either "640x480" (SD, the default) +or "1280x720" (HD). The `resolution` parameter is optional and could be included in the options +hash (second argument) of the `opentok.archives.create()` method. + +```ruby +opts = { + :output_mode => :composed, + :resolution => "1280x720" +} + +archive = opentok.archives.create session_id, opts +``` + You can stop the recording of a started Archive using the `opentok.archives.stop_by_id(archive_id)` method. You can also do this using the `Archive#stop()` method. @@ -175,14 +229,194 @@ Note that you can also create an automatically archived session, by passing in ` as the `:archive_mode` property of the `options` parameter passed into the `OpenTok#create_session()` method (see "Creating Sessions," above). +You can set the [layout](https://tokbox.com/developer/rest/#change_composed_archive_layout) of an archive: + +```ruby +opts = { :type => "verticalPresentation" } +opentok.archives.layout(archive_id, opts) +``` + +The hash `opts` has two entries: + +* The `type` is the layout type for the archive. Valid values are "bestFit" (best fit) + "custom" (custom), "horizontalPresentation" (horizontal presentation), + "pip" (picture-in-picture), and "verticalPresentation" (vertical presentation)). + +* If you specify a "custom" layout type, set the `stylesheet` property. + (For other layout types, do not set the stylesheet property.) + +See [Customizing the video layout for composed archives](https://tokbox.com/developer/guides/archiving/layout-control.html) +for more details. + +You can set the initial layout class for a client's streams by setting the layout option when you +create the token for the client, using the `opentok.generate_token` method. And you can also change +the layout classes of a stream as follows: + +```ruby +streams_list = { + :items => [ + { + :id => "8b732909-0a06-46a2-8ea8-074e64d43422", + :layoutClassList => ["full"] + }, + { + :id => "8b732909-0a06-46a2-8ea8-074e64d43423", + :layoutClassList => ["full", "focus"] + } + ] +} +response = opentok.streams.layout(session_id, streams_list) +``` + +For more information on setting stream layout classes, see the +[Changing the composed archive layout classes for an OpenTok +stream](https://tokbox.com/developer/rest/#change-stream-layout-classes-composed). + +Please keep in mind that the `streams.layout` method applies to archive and broadcast streams only. + For more information on archiving, see the [OpenTok archiving](https://tokbox.com/opentok/tutorials/archiving/) programming guide. +## Signaling -## Initiating a SIP call +You can send a signal using the `opentok.signals.send(session_id, connection_id, opts)` method. +If `connection_id` is nil or an empty string, then the signal is send to all valid connections in +the session. + +An example of `opts` field can be as follows: + +```ruby +opts = { :type => "chat", + :data => "Hello" +} +``` + +The maximum length of the `type` string is 128 bytes, and it must contain only letters +(A-Z and a-z), numbers (0-9), '-', '_', and '~'. + +The `data` string must not exceed the maximum size (8 kB). + +The `connection_id` and `opts` parameter are jointly optional by default. Hence you can also +use `opentok.signals.send(session_id)` + +For more information on signaling, see the +[OpenTok Signaling](https://tokbox.com/developer/guides/signaling/js/) programming guide. + +## Broadcasting + +You can broadcast your streams to a HLS or RTMP servers. + +To successfully start broadcasting a session, at least one publishing client must be connected to +the session. + +You can only have one active live streaming broadcast at a time for a session (however, having more +than one would not be useful). + +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 OpenTok Media Router (with the +media mode set to routed). You cannot use live streaming with sessions that have the media mode set +to relayed. + +To create a HLS only broadcast: +```ruby +opts = { + :outputs => { + :hls => {} + } +} +broadcast = opentok.broadcasts.create(session_id, opts) + +# HLS + RTMP +opts = { + :outputs => { + :hls => {}, + :rtmp => [ + { + :id => "myOpentokStream", + :serverUrl => "rtmp://x.rtmp.youtube.com/live123", + :streamName => "66c9-jwuh-pquf-9x00" + } + ] + } +} +broadcast = opentok.broadcasts.create(session_id, opts) +``` + +The returned Broadcast object has information about the broadcast, like id, sessionId , projectId, +createdAt, updatedAt, resolution, status, and a Hash of broadcastUrls. The broadcastUrls +consists of an HLS URL and an array of RTMP objects. The RTMP objects resembles the `rtmp` value +in `opts` in the example above. + +For more information on broadcast, see the +[OpenTok Broadcast guide](https://tokbox.com/developer/rest/#start_broadcast) programming guide. -You can initiate a SIP call using the `opentok.sip.dial(session_id, token, sip_uri, opts)` method. This requires a SIP url. You will often need to pass options for authenticating to the SIP provider and specifying encrypted session establishment. +To get information about a broadcast stream +```ruby +my_broadcast = opentok.broadcasts.find broadcast_id +``` +The Broadcast object returned has properties describing the broadcast, like id, sessionId, +projectId, createdAt, updatedAt, resolution, status, and a Hash of broadcastUrls. The broadcastUrls +consists of an HLS URL and an array of RTMP objects. The RTMP objects resembles the `rtmp` value +in `opts` in the example above. + +To stop a broadcast: + +```ruby + my_broadcast = opentok.broadcasts.stop broadcast_id + + # stop at a broadcast object level too + # + my_broadcast = opentok.broadcasts.find broadcast_id + ret_broadcast = my_broadcast.stop + + # Both the above returned objects has the "broadcastUrls" property as a nil value and the status + # property value is "stopped" +``` + + +To change the layout of a broadcast dynamically +```ruby +opentok.broadcasts.layout(started_broadcast_id, { + :type => "verticalPresentation" + }) + + # On an object level + my_broadcast = opentok.broadcasts.find broadcast_id + my_broadcast.layout( + :type => 'pip', + ) + + # the returned value is true if successful +``` + +The hash above has two entries. + +* The `type` is the layout type for the archive. Valid values are "bestFit" (best fit), + "custom" (custom), "horizontalPresentation" (horizontal presentation), + "pip" (picture-in-picture), and "verticalPresentation" (vertical presentation). + +* If you specify a "custom" layout type, set the `stylesheet` property. (For other layout types, + do not set the stylesheet property.) + +Refer to [Customizing the video layout for composed +archives](https://tokbox.com/developer/guides/archiving/layout-control.html) +for more details. + +You can also change the layout of an individual stream dynamically. Refer to +[working with Streams](#working-with-streams). + +## Force disconnect + +You can cause a client to be forced to disconnect from a session by using the +`opentok.connections.forceDisconnect(session_id, connection_id)` method. + +## Initiating a SIP call +You can initiate a SIP call using the `opentok.sip.dial(session_id, token, sip_uri, opts)` method. +This requires a SIP URL. You will often need to pass options for authenticating to the SIP provider +and specifying encrypted session establishment. ```ruby opts = { "auth" => { "username" => sip_username, @@ -193,7 +427,7 @@ response = opentok.sip.dial(session_id, token, "sip:+15128675309@acme.pstn.examp ``` For more information on SIP Interconnect, see the -[OpenTok SIP Interconnect](https://tokbox.com/developer/guides/sip/) programming guide. +[OpenTok SIP Interconnect](https://tokbox.com/developer/guides/sip/) developer guide. # Samples @@ -210,7 +444,8 @@ Reference documentation is available at . +You need an OpenTok API key and API secret, which you can obtain by logging into your +[TokBox account](https://tokbox.com/account). The OpenTok Ruby SDK requires Ruby 1.9.3 or greater. diff --git a/lib/opentok/archive.rb b/lib/opentok/archive.rb index 22c7716c..2ce0fa71 100644 --- a/lib/opentok/archive.rb +++ b/lib/opentok/archive.rb @@ -54,8 +54,8 @@ module OpenTok # * "started" -- The archive started and is in the process of being recorded. # * "stopped" -- The archive stopped recording. # * "uploaded" -- The archive is available for download from the the upload target - # Amazon S3 bucket or Windows Azure container you set at the OpenTok dashboard - # (https://dashboard.tokbox.com). + # Amazon S3 bucket or Windows Azure container you set for your + # {https://tokbox.com/account OpenTok project}. # # @attr [string] url # The download URL of the available MP4 file. This is only set for an archive with the status set to @@ -71,14 +71,14 @@ def initialize(interface, json) @json = json end - # A JSON encoded string representation of the archive + # A JSON-encoded string representation of the archive. def to_json @json.to_json end # Stops an OpenTok archive that is being recorded. # - # Archives automatically stop recording after 90 minutes or when all clients have disconnected + # Archives automatically stop recording after 120 minutes or when all clients have disconnected # from the session being archived. def stop # TODO: validate returned json fits schema @@ -95,6 +95,47 @@ def delete @json = @interface.delete_by_id @json['id'] end + # Sets the layout type for a composed archive. For a description of layout types, see + # {https://tokbox.com/developer/guides/archiving/layout-control.html Customizing + # the video layout for composed archives}. + # + # @option options [String] :type + # The layout type. Set this to "bestFit", "pip", "verticalPresentation", + # "horizontalPresentation", "focus", or "custom". + # + # @option options [String] :stylesheet + # The stylesheet for a custom layout. Set this parameter + # if you set type to "custom". Otherwise, leave it undefined. + # + # @raise [ArgumentError] The archive_id or options parameter is empty. Or the "custom" + # type was specified without a stylesheet option. Or a stylesheet was passed in for a + # type other than custom. Or an invalid type was passed in. + # + # @raise [OpenTokAuthenticationError] + # Authentication failed. + # + # @raise [ArgumentError] + # The archive_id or options parameter is empty. + # + # @raise [ArgumentError] + # The "custom" type was specified without a stylesheet option. + # + # @raise [ArgumentError] + # A stylesheet was passed in for a type other than custom. Or an invalid type was passed in. + # + # @raise [ArgumentError] + # An invalid layout type was passed in. + # + # @raise [OpenTokError] + # OpenTok server error. + # + # @raise [OpenTokArchiveError] + # Setting the layout failed. + def layout(opts= {}) + # TODO: validate returned json fits schema + @json = @interface.layout(@json['id'], opts) + end + # @private ignore def method_missing(method, *args, &block) camelized_method = method.to_s.camelize(:lower) diff --git a/lib/opentok/archives.rb b/lib/opentok/archives.rb index 18b3360b..49f2c606 100644 --- a/lib/opentok/archives.rb +++ b/lib/opentok/archives.rb @@ -24,8 +24,8 @@ def initialize(client) # {https://tokbox.com/opentok/tutorials/archiving OpenTok archiving} programming guide. # # @param [String] session_id The session ID of the OpenTok session to archive. - # @param [Hash] options A hash with the key 'name', 'has_audio', and 'has_video' (or - # :name. + # @param [Hash] options A hash with the keys 'name', 'has_audio', 'has_video', + # and 'output_mode'. # @option options [String] :name This is the name of the archive. You can use this name # to identify the archive. It is a property of the Archive object, and it is a property # of archive-related events in the OpenTok client SDKs. @@ -44,6 +44,10 @@ def initialize(client) # (:individual). For more information on archiving and the archive file # formats, see the {https://tokbox.com/opentok/tutorials/archiving OpenTok archiving} # programming guide. + # @option options [String] :resolution The resolution of the archive, either "640x480" (SD, the + # default) or "1280x720" (HD). This property only applies to composed archives. If you set + # this property and set the outputMode property to "individual", the call the method + # results in an error. # # @return [Archive] The Archive object, which includes properties defining the archive, # including the archive ID. @@ -58,9 +62,11 @@ def initialize(client) # @raise [OpenTokArchiveError] The archive could not be started. def create(session_id, options = {}) raise ArgumentError, "session_id not provided" if session_id.to_s.empty? + raise ArgumentError, + "Resolution cannot be supplied for individual output mode" if options.key?(:resolution) and options[:output_mode] == :individual # normalize opts so all keys are symbols and only include valid_opts - valid_opts = [ :name, :has_audio, :has_video, :output_mode ] + valid_opts = [ :name, :has_audio, :has_video, :output_mode, :resolution ] opts = options.inject({}) do |m,(k,v)| if valid_opts.include? k.to_sym m[k.to_sym] = v @@ -97,7 +103,7 @@ def find(archive_id) # @option options [integer] :count Optional. The number of archives to be returned. The maximum # number of archives returned is 1000. # @option options [String] :session_id Optional. The session ID that archives belong to. This is - # useful when listing multiple archives for an {https://tokbox.com/developer/guides/archiving/#automatic-archives automatically archived session} + # useful when listing multiple archives for an {https://tokbox.com/developer/guides/archiving/#automatic-archives automatically archived session}. # # @return [ArchiveList] An ArchiveList object, which is an array of Archive objects. def all(options = {}) @@ -108,7 +114,7 @@ def all(options = {}) # Stops an OpenTok archive that is being recorded. # - # Archives automatically stop recording after 90 minutes or when all clients have disconnected + # Archives automatically stop recording after 120 minutes or when all clients have disconnected # from the session being archived. # # @param [String] archive_id The archive ID of the archive you want to stop recording. @@ -146,5 +152,56 @@ def delete_by_id(archive_id) (200..300).include? response.code end + # Sets the layout type for a composed archive. For a description of layout types, see + # {https://tokbox.com/developer/guides/archiving/layout-control.html Customizing + # the video layout for composed archives}. + # + # @param [String] archive_id + # The archive ID. + # + # @option options [String] :type + # The layout type. Set this to "bestFit", "pip", "verticalPresentation", + # "horizontalPresentation", "focus", or "custom". + # + # @option options [String] :stylesheet + # The stylesheet for a custom layout. Set this parameter + # if you set type to "custom". Otherwise, leave it undefined. + # + # @raise [ArgumentError] + # The archive_id or options parameter is empty. Or the "custom" + # type was specified without a stylesheet option. Or a stylesheet was passed in for a + # type other than custom. Or an invalid type was passed in. + # + # @raise [OpenTokAuthenticationError] + # Authentication failed. + # + # @raise [ArgumentError] + # The archive_id or options parameter is empty. + # + # @raise [ArgumentError] + # The "custom" type was specified without a stylesheet option. + # + # @raise [ArgumentError] + # A stylesheet was passed in for a type other than custom. Or an invalid type was passed in. + # + # @raise [ArgumentError] + # An invalid layout type was passed in. + # + # @raise [OpenTokError] + # OpenTok server error. + # + # @raise [OpenTokArchiveError] + # Setting the layout failed. + def layout(archive_id, options = {}) + raise ArgumentError, "option parameter is empty" if options.empty? + raise ArgumentError, "archive_id not provided" if archive_id.to_s.empty? + type = options[:type] + raise ArgumentError, "custom type must have a stylesheet" if (type.eql? "custom") && (!options.key? :stylesheet) + valid_non_custom_type = ["bestFit","horizontalPresentation","pip", "verticalPresentation", ""].include? type + raise ArgumentError, "type is not valid or stylesheet not needed" if !valid_non_custom_type + raise ArgumentError, "type is not valid or stylesheet not needed" if valid_non_custom_type and options.key? :stylesheet + response = @client.layout_archive(archive_id, options) + (200..300).include? response.code + end end end diff --git a/lib/opentok/broadcast.rb b/lib/opentok/broadcast.rb new file mode 100644 index 00000000..a2b54b21 --- /dev/null +++ b/lib/opentok/broadcast.rb @@ -0,0 +1,118 @@ +require "active_support/inflector" + +module OpenTok + # Represents a live streaming broadcast of an OpenTok session. + # See {https://tokbox.com/developer/guides/broadcast/live-streaming/ Live streaming broadcasts}. + # + # @attr [string] id + # The broadcast ID. + # + # @attr [string] session_id + # The session ID of the OpenTok session associated with this broadcast. + # + # @attr [string] project_id + # The API key associated with the broadcast. + # + # @attr [int] created_at + # The time at which the broadcast was created, in milliseconds since the UNIX epoch. + # + # @attr [int] updated_at + # For this start method, this timestamp matches the createdAt timestamp. + # + # @attr [string] resolution + # The resolution of the broadcast: either "640x480" (SD, the default) or "1280x720" (HD). This property is optional. + # + # @attr [Hash] broadcastUrls is defined as follows: + # This object defines the types of broadcast streams you want to start (both HLS and RTMP). + # You can include HLS, RTMP, or both as broadcast streams. If you include RTMP streaming, + # you can specify up to five target RTMP streams (or just one). + # The (:hls) property is set to an empty [Hash] object. The HLS URL is returned in the response. + # The (:rtmp) property is set to an [Array] of Rtmp [Hash] properties. + # For each RTMP stream, specify (:serverUrl) for the RTMP server URL, + # (:streamName) such as the YouTube Live stream name or the Facebook stream key), + # and (optionally) (:id), a unique ID for the stream. + # + # @attr [string] status The status of the RTMP stream. + # * "connecting" -- The OpenTok platform is in the process of connecting to the remote RTMP server. + # This is the initial state, and it is the status if you start when there are no streams published in the session. + # It changes to "live" when there are streams (or it changes to one of the other states). + # * "live -- The OpenTok platform has successfully connected to the remote RTMP server, and the media is streaming. + # * "offline" -- The OpenTok platform could not connect to the remote RTMP server. This is due to an unreachable server or an error in the RTMP handshake. Causes include rejected RTMP connections, non-existing RTMP applications, rejected stream names, authentication errors, etc. Check that the server is online, and that you have provided the correct server URL and stream name. + # * "error" -- There is an error in the OpenTok platform. + class Broadcast + + # @private + def initialize(interface, json) + @interface = interface + # TODO: validate json fits schema + @json = json + end + + # A JSON-encoded string representation of the broadcast. + def to_json + @json.to_json + end + + # Stops the OpenTok broadcast. + def stop + # TODO: validate returned json fits schema + @json = @interface.stop @json['id'] + end + + # Sets the layout of the OpenTok broadcast. + # + # You can dynamically change the layout type of a broadcast while it is being broadcast. + # For more information, see + # {https://tokbox.com/developer/guides/broadcast/live-streaming/#configuring-video-layout-for-opentok-live-streaming-broadcasts Configuring video layout for OpenTok live streaming broadcasts}. + # + # @option options [String] :type + # The layout type. Set this to "bestFit", "pip", "verticalPresentation", + # "horizontalPresentation", "focus", or "custom". + # + # @option options [String] :stylesheet + # The stylesheet for a custom layout. Set this parameter + # if you set type to "custom". Otherwise, leave it undefined. + # + # @raise [OpenTokBroadcastError] + # The broadcast layout could not be updated. + # + # @raise [OpenTokAuthenticationError] + # Authentication failed. Invalid API key or secret. + # + # @raise [OpenTokError] + # OpenTok server error. + # + # @raise [ArgumentError] + # The broadcast_id or options parameter is empty. + # + # @raise [ArgumentError] + # The "custom" type was specified without a stylesheet option. + # + # @raise [ArgumentError] + # A stylesheet was passed in for a type other than custom. Or an invalid type was passed in. + # + # @raise [ArgumentError] + # An invalid layout type was passed in. + # Refer to {https://tokbox.com/developer/rest/#change_composed_archive_layout} + + def layout(opts = {}) + # TODO: validate returned json fits schema + @json = @interface.layout(@json['id'], opts) + end + + # @private ignore + def method_missing(method, *args, &block) + camelized_method = method.to_s.camelize(:lower) + if @json.has_key? camelized_method and args.empty? + # TODO: convert create_time method call to a Time object + if camelized_method == 'outputMode' + @json[camelized_method].to_sym + else + @json[camelized_method] + end + else + super method, *args, &block + end + end + end +end diff --git a/lib/opentok/broadcasts.rb b/lib/opentok/broadcasts.rb new file mode 100644 index 00000000..6b77c1f2 --- /dev/null +++ b/lib/opentok/broadcasts.rb @@ -0,0 +1,149 @@ +require "opentok/client" +require "opentok/broadcast" + + +module OpenTok + # A class for working with OpenTok live streaming broadcasts. + # See {https://tokbox.com/developer/guides/broadcast/live-streaming/ Live streaming broadcasts}. + class Broadcasts + + # @private + def initialize(client) + @client = client + end + + # Starts a live streaming broadcast of an OpenTok session. + # + # Clients must be actively connected to the OpenTok session for you to successfully start + # a broadcast. + # + # This broadcasts the session to an HLS (HTTP live streaming) or to RTMP streams. + # + # @param [String] session_id The session ID of the OpenTok session to broadcast. + # + # @param [Hash] options A hash defining options for the broadcast. + # @option options [Hash] :layout Specify this to assign the initial layout for the broadcast. + # Valid values for the layout (:type) property are "bestFit" (best fit), "custom" (custom), + # "horizontalPresentation" (horizontal presentation), "pip" (picture-in-picture), and + # "verticalPresentation" (vertical presentation)). + # If you specify a (:custom) layout type, set the (:stylesheet) property of the layout object + # to the stylesheet. (For other layout types, do not set a stylesheet property.) + # If you do not specify an initial layout type, the broadcast stream uses the Best Fit layout type. + # + # @option options [int] maxDuration + # The maximum duration for the broadcast, in seconds. The broadcast will automatically stop when + # the maximum duration is reached. You can set the maximum duration to a value from 60 (60 seconds) to 36000 (10 hours). + # The default maximum duration is 2 hours (7200 seconds). + # + # @option options [Hash] outputs + # This object defines the types of broadcast streams you want to start (both HLS and RTMP). + # You can include HLS, RTMP, or both as broadcast streams. If you include RTMP streaming, + # you can specify up to five target RTMP streams (or just one). + # The (:hls) property is set to an empty [Hash] object. The HLS URL is returned in the response. + # The (:rtmp) property is set to an [Array] of Rtmp [Hash] properties. + # For each RTMP , specify (:serverUrl) for the RTMP server URL, + # (:streamName) such as the YouTube Live stream name or the Facebook stream key), + # and (optionally) (:id), a unique ID for the stream. + # + # @option options [string] resolution + # The resolution of the broadcast: either "640x480" (SD, the default) or "1280x720" (HD). + # + # @return [Broadcast] The broadcast object, which includes properties defining the broadcast, + # including the broadcast ID. + # + # @raise [OpenTokBroadcastError] The broadcast could not be started. The request was invalid or broadcast already started + # @raise [OpenTokAuthenticationError] Authentication failed while starting an archive. + # Invalid API key. + # @raise [OpenTokError] OpenTok server error. + def create(session_id, options = {}) + raise ArgumentError, "session_id not provided" if session_id.to_s.empty? + raise ArgumentError, "options cannot be empty" if options.empty? + broadcast_json = @client.start_broadcast(session_id, options) + Broadcast.new self, broadcast_json + end + + # Gets a Broadcast object for the given broadcast ID. + # + # @param [String] broadcast_id The broadcast ID. + # + # @return [Broadcast] The broadcast object, which includes properties defining the broadcast. + # + # @raise [OpenTokBroadcastError] No matching broadcast found. + # @raise [OpenTokAuthenticationError] Authentication failed. + # Invalid API key. + # @raise [OpenTokError] OpenTok server error. + def find(broadcast_id) + raise ArgumentError, "broadcast_id not provided" if broadcast_id.to_s.empty? + broadcast_json = @client.get_broadcast(broadcast_id.to_s) + Broadcast.new self, broadcast_json + end + + + # Stops an OpenTok broadcast + # + # Note that broadcasts automatically stop after 120 minute + # + # @param [String] broadcast_id The broadcast ID. + # + # @return [Broadcast] The broadcast object, which includes properties defining the broadcast. + # + # @raise [OpenTokBroadcastError] The broadcast could not be stopped. The request was invalid. + # @raise [OpenTokAuthenticationError] Authentication failed. + # Invalid API key. + # @raise [OpenTokError] OpenTok server error. + def stop(broadcast_id) + raise ArgumentError, "broadcast_id not provided" if broadcast_id.to_s.empty? + broadcast_json = @client.stop_broadcast(broadcast_id) + Broadcast.new self, broadcast_json + end + + # Dynamically alters the layout an OpenTok broadcast. For more information, see + # For more information, see + # {https://tokbox.com/developer/guides/broadcast/live-streaming/#configuring-video-layout-for-opentok-live-streaming-broadcasts Configuring video layout for OpenTok live streaming broadcasts}. + # + # @param [String] broadcast_id + # The broadcast ID. + # + # @option options [String] :type + # The layout type. Set this to "bestFit", "pip", "verticalPresentation", + # "horizontalPresentation", "focus", or "custom". + # + # @option options [String] :stylesheet + # The stylesheet for a custom layout. Set this parameter + # if you set type to "custom". Otherwise, leave it undefined. + # + # @raise [OpenTokBroadcastError] + # The broadcast layout could not be updated. + # + # @raise [OpenTokAuthenticationError] + # Authentication failed. Invalid API key or secret. + # + # @raise [OpenTokError] + # OpenTok server error. + # + # @raise [ArgumentError] + # The broadcast_id or options parameter is empty. + # + # @raise [ArgumentError] + # The "custom" type was specified without a stylesheet option. + # + # @raise [ArgumentError] + # A stylesheet was passed in for a type other than custom. Or an invalid type was passed in. + # + # @raise [ArgumentError] + # An invalid layout type was passed in. + def layout(broadcast_id, options = {}) + raise ArgumentError, "option parameter is empty" if options.empty? + raise ArgumentError, "broadcast_id not provided" if broadcast_id.to_s.empty? + type = options[:type] + raise ArgumentError, "custom type must have a stylesheet" if (type.eql? "custom") && (!options.key? :stylesheet) + valid_non_custom_type = ["bestFit","horizontalPresentation","pip", "verticalPresentation", ""].include? type + raise ArgumentError, "type is not valid" if !valid_non_custom_type + raise ArgumentError, "stylesheet not needed" if valid_non_custom_type and options.key? :stylesheet + response = @client.layout_broadcast(broadcast_id, options) + (200..300).include? response.code + end + + + end +end diff --git a/lib/opentok/client.rb b/lib/opentok/client.rb index 91d78a7b..9129458c 100644 --- a/lib/opentok/client.rb +++ b/lib/opentok/client.rb @@ -164,6 +164,72 @@ def delete_archive(archive_id) raise OpenTokError, "Failed to connect to OpenTok. Response code: #{e.message}" end + def layout_archive(archive_id, opts) + opts.extend(HashExtensions) + response = self.class.put("/v2/project/#{@api_key}/archive/#{archive_id}/layout", { + :body => opts.camelize_keys!.to_json, + :headers => generate_headers("Content-Type" => "application/json") + }) + case response.code + when 200 + response + when 400 + raise OpenTokArchiveError, "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 500 + raise OpenTokError, "Setting the layout failed. OpenTok server error." + else + raise OpenTokArchiveError, "Setting the layout failed." + end + rescue StandardError => e + 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}", { + :headers => generate_headers("Content-Type" => "application/json") + }) + case response.code + when 204 + response + when 400 + raise ArgumentError, "Force disconnect failed. Connection ID #{connection_id} or Session ID #{session_id} is invalid" + when 403 + raise OpenTokAuthenticationError, "You are not authorized to forceDisconnect, check your authentication credentials or token type is non-moderator" + when 404 + 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 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 ArgumentError, "One of the signal properties — data, type, sessionId or connectionId — is invalid." + when 403 + raise OpenTokAuthenticationError, "You are not authorized to send the signal. Check your authentication credentials." + 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)." + else + 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 dial(session_id, token, sip_uri, opts) opts.extend(HashExtensions) body = { "sessionId" => session_id, @@ -179,14 +245,151 @@ def dial(session_id, token, sip_uri, opts) when 200 response when 403 - raise OpenTokAuthenticationError, "Authentication failed while dialing a sip session. API Key: #{@api_key}" + raise OpenTokAuthenticationError, "Authentication failed while dialing a SIP session. API Key: #{@api_key}" when 404 - raise OpenTokSipError, "The sip session could not be dialed. The Session ID does not exist: #{session_id}" + raise OpenTokSipError, "The SIP session could not be dialed. The Session ID does not exist: #{session_id}" else - raise OpenTokSipError, "The sip session could not be dialed" + 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 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 ArgumentError, 'Invalid request. You did not pass in a valid session ID or stream ID.' + 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.' + when 500 + raise OpenTokError, 'OpenTok server error.' + else + 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 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") + }) + case response.code + when 200 + response + when 400 + 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 500 + raise OpenTokError, "Setting the layout failed. OpenTok server error." + else + raise OpenTokStreamLayoutError, "Setting the layout failed." + 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") + }) + 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." + 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 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 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 OpenTokBroadcastError, "The request was invalid." + when 403 + raise OpenTokAuthenticationError, "Authentication failed while stopping a broadcast. 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." + 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 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") + }) + 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." + when 403 + 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 + end end diff --git a/lib/opentok/connections.rb b/lib/opentok/connections.rb new file mode 100644 index 00000000..5a5fa7c0 --- /dev/null +++ b/lib/opentok/connections.rb @@ -0,0 +1,28 @@ +module OpenTok + # A class for working with OpenTok connections. + class Connections + # @private + def initialize(client) + @client = client + end + + # Force a client to disconnect from an OpenTok session. + # + # A client must be actively connected to the OpenTok session for you to disconnect it. + # + # @param [String] session_id The session ID of the OpenTok session. + # @param [String] connection_id The connection ID of the client in the session. + # + # @raise [ArgumentError] The connection_id or session_id is invalid. + # @raise [OpenTokAuthenticationError] You are not authorized to disconnect the connection. Check your authentication credentials. + # @raise [OpenTokConnectionError] The client specified by the connection_id property is not connected to the session. + # + def forceDisconnect(session_id, connection_id ) + raise ArgumentError, "session_id not provided" if session_id.to_s.empty? + raise ArgumentError, "connection_id not provided" if connection_id.to_s.empty? + response = @client.forceDisconnect(session_id, connection_id) + (200..300).include? response.code + end + + end +end \ No newline at end of file diff --git a/lib/opentok/exceptions.rb b/lib/opentok/exceptions.rb index 5af84072..65ddea8d 100644 --- a/lib/opentok/exceptions.rb +++ b/lib/opentok/exceptions.rb @@ -8,5 +8,11 @@ class OpenTokArchiveError < OpenTokError; end class OpenTokSipError < OpenTokError; end # Defines errors raised when you attempt an operation using an invalid OpenTok API key or secret. class OpenTokAuthenticationError < OpenTokError; end + # Defines errors raised when you attempt a force disconnect a client and it is not connected to the session. + class OpenTokConnectionError < OpenTokError; end + # Defines errors raised when you attempt set layout classes to a stream. + class OpenTokStreamLayoutError < OpenTokError; end + # Defines errors raised when you perform Broadcast operations. + class OpenTokBroadcastError < OpenTokError; end end diff --git a/lib/opentok/opentok.rb b/lib/opentok/opentok.rb index 560e58fd..d7311c03 100644 --- a/lib/opentok/opentok.rb +++ b/lib/opentok/opentok.rb @@ -2,17 +2,24 @@ require "opentok/session" require "opentok/client" require "opentok/token_generator" +require "opentok/connections" require "opentok/archives" require "opentok/sip" +require "opentok/streams" +require "opentok/signals" +require "opentok/broadcasts" require "resolv" require "set" module OpenTok - # Contains methods for creating OpenTok sessions, generating tokens, and working with archives. + # Contains methods for creating OpenTok sessions and generating tokens. It also includes + # methods for returning object that let you work with archives, work with live streaming + # broadcasts, using SIP interconnect, sending signals to sessions, disconnecting clients from + # sessions, and setting the layout classes for streams. # # To create a new OpenTok object, call the OpenTok constructor with your OpenTok API key - # and the API secret from the OpenTok dashboard (https://dashboard.tokbox.com). Do not + # and the API secret for your {https://tokbox.com/account OpenTok project}. Do not # publicly share your API secret. You will use it with the OpenTok constructor (only on your web # server) to create OpenTok sessions. # @@ -20,7 +27,7 @@ module OpenTok # @attr_reader [String] api_key @private The OpenTok API key. # # - # @!method generate_token(options) + # @!method generate_token(session_id, options) # Generates a token for a given session. # # @param [String] session_id The session ID of the session to be accessed by the client using @@ -45,6 +52,11 @@ module OpenTok # end-user. For example, you can pass the user ID, name, or other data describing the # end-user. The length of the string is limited to 1000 characters. This data cannot be # updated once it is set. + # @option options [Array] :initial_layout_class_list + # An array of class names (strings) to be used as the initial layout classes for streams + # published by the client. Layout classes are used in customizing the layout of videos in + # {https://tokbox.com/developer/guides/broadcast/live-streaming/ live streaming broadcasts} + # and {https://tokbox.com/developer/guides/archiving/layout-control.html composed archives}. # @return [String] The token string. class OpenTok @@ -62,8 +74,8 @@ class OpenTok ## # Create a new OpenTok object. # - # @param [String] api_key Your OpenTok API key. See the OpenTok dashboard - # (https://dashboard.tokbox.com). + # @param [String] api_key The OpenTok API key for your + # {https://tokbox.com/account OpenTok project}. # @param [String] api_secret Your OpenTok API key. # @option opts [Symbol] :api_url Do not set this parameter. It is for internal use by TokBox. # @option opts [Symbol] :ua_addendum Do not set this parameter. It is for internal use by TokBox. @@ -89,8 +101,8 @@ def initialize(api_key, api_secret, opts={}) # Check the error message for details. # # You can also create a session using the OpenTok REST API (see - # http://www.tokbox.com/opentok/api/#session_id_production) or the OpenTok dashboard - # (see https://dashboard.tokbox.com/projects). + # http://www.tokbox.com/opentok/api/#session_id_production) or at your + # {https://tokbox.com/account OpenTok account page}. # # @param [Hash] opts (Optional) This hash defines options for the session. # @@ -173,12 +185,32 @@ def archives @archives ||= Archives.new client end + # A Broadcasts object, which lets you work with OpenTok live streaming broadcasts. + def broadcasts + @broadcasts ||= Broadcasts.new client + end + + # A Sip object, which lets you use the OpenTok SIP gateway. def sip @sip ||= Sip.new client end - protected + # A Streams object, which lets you work with OpenTok live streaming broadcasts. + def streams + @streams ||= Streams.new client + end + # A Signals object, which lets you send signals to OpenTok sessions. + def signals + @signals ||= Signals.new client + end + + # A Connections object, which lets disconnect clients from an OpenTok session. + def connections + @connections ||= Connections.new client + end + + protected def client @client ||= Client.new api_key, api_secret, api_url, ua_addendum end diff --git a/lib/opentok/session.rb b/lib/opentok/session.rb index 30b6eb4d..c3a60686 100644 --- a/lib/opentok/session.rb +++ b/lib/opentok/session.rb @@ -42,6 +42,11 @@ module OpenTok # end-user. For example, you can pass the user ID, name, or other data describing the # end-user. The length of the string is limited to 1000 characters. This data cannot be # updated once it is set. + # @option options [Array] :initial_layout_class_list + # An array of class names (strings) to be used as the initial layout classes for streams + # published by the client. Layout classes are used in customizing the layout of videos in + # {https://tokbox.com/developer/guides/broadcast/live-streaming/ live streaming broadcasts} + # and {https://tokbox.com/developer/guides/archiving/layout-control.html composed archives}. # @return [String] The token string. class Session diff --git a/lib/opentok/signals.rb b/lib/opentok/signals.rb new file mode 100644 index 00000000..b35b719a --- /dev/null +++ b/lib/opentok/signals.rb @@ -0,0 +1,47 @@ +module OpenTok + # A class for working with OpenTok signals. + class Signals + # @private + def initialize(client) + @client = client + end + + # Sends a signal to clients connected to an OpenTok session. + # + # You can send a signal to all valid connections in a session or to a specific connection of + # a session. + # + # For more information on signaling, see + # {https://tokbox.com/developer/rest/#send_signal}. + # + # @param [String] session_id The session ID of the OpenTok session. + # + # @param [String] connection_id + # When a connection_id is specified, only that connection recieves the signal. + # Otherwise, the signal is sent to all clients connected to the session. + # + # @option options [String] :type This is the type of the signal. You can use this + # field to group and filter signals. It is a property of the Signal object received by + # the client(s). + # + # @option options [String] :data This is the data within the signal or the payload. + # Contains the main information to be sent in the signal. It is a property of the Signal object + # received by the client(s). + # + # @raise [ArgumentError] + # One of the signal properties — data, type, session_id, or connection_id — is invalid. + # @raise [OpenTokAuthenticationError] + # You are not authorized to send the signal. Check your authentication credentials. + # @raise [OpenTokError] + # The client specified by the connection_id property is not connected to the session. + # @raise [OpenTokError] + # The type string exceeds the maximum length (128 bytes), or the data string exceeds + # the maximum size (8 kB). + def send(session_id, connectionId = "", options = {}) + raise ArgumentError, "session_id not provided" if session_id.to_s.empty? + response = @client.signal(session_id, connectionId, options) + (200..300).include? response.code + end + + end +end \ No newline at end of file diff --git a/lib/opentok/sip.rb b/lib/opentok/sip.rb index 705597df..a058c418 100644 --- a/lib/opentok/sip.rb +++ b/lib/opentok/sip.rb @@ -1,7 +1,42 @@ require "opentok/client" +# An object that lets you use the OpenTok SIP gateway. module OpenTok class Sip + # Dials a SIP gateway to input an audio-only stream into your OpenTok session. + # See the {https://tokbox.com/developer/guides/sip/ OpenTok SIP developer guide}. + # + # @example + # opts = { "from" => "14155550101@example.com", + # "auth" => { "username" => sip_username, + # "password" => sip_password }, + # "headers" => { "X-KEY1" => "value1", + # "X-KEY1" => "value2" }, + # "secure" => "true" + # } + # response = opentok.sip.dial(session_id, token, "sip:+15128675309@acme.pstn.example.com;transport=tls", opts) + # @param [String] session_id The session ID corresponding to the session to which + # the SIP gateway will connect. + # @param [String] token The token for the session ID with which the SIP user + # will use to connect. + # @param [String] sip_uri The SIP URI the OpenTok SIP gateway will dial. + # @param [Hash] opts A hash defining options for the SIP call. For example: + # @option opts [String] :from The number or string that will be sent to the final + # SIP number as the caller. It must be a string in the form of "from@example.com", + # where from can be a string or a number. If from is set to a number + # (for example, "14155550101@example.com"), it will show up as the incoming + # number on PSTN phones. If from is undefined or set to a string (for example, + # "joe@example.com"), +00000000 will show up as the incoming number on + # PSTN phones. + # @option opts [Hash] :headers This hash defines custom headers to be added + # to the SIP ​INVITE​ request initiated from OpenTok to the your SIP platform. + # Each of the custom headers must start with the ​"X-"​ prefix, or the call + # will result in a Bad Request (400) response. + # @option opts [Hash] :auth This object contains the username and password + # to be used in the the SIP INVITE​ request for HTTP digest authentication, + # if it is required by your SIP platform. + # @option opts [true, false] :secure Wether the media must be transmitted + # encrypted (​true​) or not (​false​, the default). def dial(session_id, token, sip_uri, opts) response = @client.dial(session_id, token, sip_uri, opts) end diff --git a/lib/opentok/stream.rb b/lib/opentok/stream.rb new file mode 100644 index 00000000..a5b6a1db --- /dev/null +++ b/lib/opentok/stream.rb @@ -0,0 +1,46 @@ +require "active_support/inflector" + +module OpenTok + # Represents information about a stream in an OpenTok session. + # + # @attr [string] id + # The stream ID. + # + # @attr [string] name + # The name of the stream. + + # @attr [string] videoType + # The videoType property is either "camera" or "screen". + # + # @attr [array] layoutClassList + # An array of the layout classes for the stream. + class Stream + + # @private + def initialize(json) + # TODO: validate json fits schema + @json = json + end + + # A JSON-encoded string representation of the stream. + def to_json + @json.to_json + end + + + # @private ignore + def method_missing(method, *args, &block) + camelized_method = method.to_s.camelize(:lower) + if @json.has_key? camelized_method and args.empty? + # TODO: convert create_time method call to a Time object + if camelized_method == 'outputMode' + @json[camelized_method].to_sym + else + @json[camelized_method] + end + else + super method, *args, &block + end + end + end +end diff --git a/lib/opentok/stream_list.rb b/lib/opentok/stream_list.rb new file mode 100644 index 00000000..6c5e951e --- /dev/null +++ b/lib/opentok/stream_list.rb @@ -0,0 +1,18 @@ +require "opentok/stream" + + +module OpenTok + # A class for accessing a list of Stream objects. + class StreamList < Array + + # The total number streams. + attr_reader :total + + # @private + def initialize(json) + @total = json['count'] + super json['items'].map { |item| Stream.new item } + end + + end +end \ No newline at end of file diff --git a/lib/opentok/streams.rb b/lib/opentok/streams.rb new file mode 100644 index 00000000..59c91fde --- /dev/null +++ b/lib/opentok/streams.rb @@ -0,0 +1,79 @@ +require 'opentok/client' +require 'opentok/stream' +require 'opentok/stream_list' + +module OpenTok + # A class for working with OpenTok streams. It includes methods for getting info + # about OpenTok streams and for setting layout classes for streams. + class Streams + # @private + def initialize(client) + @client = client + end + + # Use this method to get information on an OpenTok stream. + # + # For example, you can call this method to get information about layout classes used by an OpenTok stream. + # The layout classes define how the stream is displayed in the layout of a broadcast stream. + # For more information, see {https://tokbox.com/developer/guides/broadcast/live-streaming/#assign-layout-classes-to-streams Assigning layout classes to streams in live streaming broadcasts} + # and {https://tokbox.com/developer/guides/archiving/layout-control.html Customizing the video layout for composed archives}. + # + # @param [String] session_id The session ID of the OpenTok session. + # @param [String] stream_id The stream ID within the session. + # @return [Stream] The Stream object. + # @raise [ArgumentError] stream_id or session_id is invalid. + # @raise [OpenTokAuthenticationError] You are not authorized to fetch the stream information. Check your authentication credentials. + # @raise [OpenTokError] An OpenTok server error. + # + def find(session_id, stream_id) + raise ArgumentError, 'session_id not provided' if session_id.to_s.empty? + raise ArgumentError, 'stream_id not provided' if session_id.to_s.empty? + stream_json = @client.info_stream(session_id, stream_id) + Stream.new stream_json + end + + # Use this method to get information on all OpenTok streams in a session. + # + # For example, you can call this method to get information about layout classes used by OpenTok streams. + # The layout classes define how the stream is displayed in the layout of a live streaming + # broadcast or a composed archive. For more information, see + # {https://tokbox.com/developer/guides/broadcast/live-streaming/#assign-layout-classes-to-streams Assigning layout classes to streams in live streaming broadcasts} + # and {https://tokbox.com/developer/guides/archiving/layout-control.html Customizing the video layout for composed archives}. + # + # @param [String] session_id The session ID of the OpenTok session. + # @return [StreamList] The StreamList of Stream objects. + # @raise [ArgumentError] The stream_id or session_id is invalid. + # @raise [OpenTokAuthenticationError] You are not authorized to fetch the stream information. Check your authentication credentials. + # @raise [OpenTokError] An OpenTok server error. + # + def all(session_id) + raise ArgumentError, 'session_id not provided' if session_id.to_s.empty? + response_json = @client.info_stream(session_id, '') + StreamList.new response_json + end + + # Use this method to set the layout of a composed (archive or broadcast) OpenTok stream. + # + # For example, you can call this method to set the layout classes of an OpenTok stream. + # The layout classes define how the stream is displayed in the layout of a live streaming + # broadcast or a composed archive. For more information, see + # {https://tokbox.com/developer/guides/broadcast/live-streaming/#assign-layout-classes-to-streams Assigning layout classes to streams in live streaming broadcasts} + # and {https://tokbox.com/developer/guides/archiving/layout-control.html Customizing the video layout for composed archives}. + # + # @param [String] session_id The session ID of the OpenTok session. + # @param [Hash] opts A hash with one key items and value as array of objects having stream_id and layoutClassList properties. + # For more information, see Layout{https://tokbox.com/developer/rest/#change-stream-layout-classes-composed} + # @raise [ArgumentError] The session_id is invalid. + # @raise [OpenTokAuthenticationError] You are not authorized to fetch the stream information. Check your authentication credentials. + # @raise [OpenTokStreamLayoutError] The layout operation could not be performed due to incorrect layout values. + # @raise [OpenTokError] An OpenTok server error. + # + def layout(session_id, opts) + raise ArgumentError, 'session_id not provided' if session_id.to_s.empty? + raise ArgumentError, 'opts is empty' if opts.empty? + response = @client.layout_streams(session_id, opts) + (200..300).include? response.code + end + + end +end \ No newline at end of file diff --git a/lib/opentok/version.rb b/lib/opentok/version.rb index 9735f184..07c90530 100644 --- a/lib/opentok/version.rb +++ b/lib/opentok/version.rb @@ -1,4 +1,4 @@ module OpenTok # @private - VERSION = '3.0.3' + VERSION = '3.1.0' end diff --git a/spec/cassettes/OpenTok_Archives/calls_layout_on_archive_object.yml b/spec/cassettes/OpenTok_Archives/calls_layout_on_archive_object.yml new file mode 100644 index 00000000..e354f463 --- /dev/null +++ b/spec/cassettes/OpenTok_Archives/calls_layout_on_archive_object.yml @@ -0,0 +1,45 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.opentok.com/v2/project/123456/archive/f6e7ee58-d6cf-4a59-896b-6d56b158ec71 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 26 Sep 2018 18:07:55 GMT + Content-Type: + - application/json + Connection: + - keep-alive + body: + encoding: UTF-8 + string: |- + { + "createdAt" : 1395187836000, + "duration" : 62, + "id" : "f6e7ee58-d6cf-4a59-896b-6d56b158ec71", + "name" : "", + "partnerId" : 123456, + "reason" : "", + "sessionId" : "SESSIONID", + "size" : 8347554, + "status" : "available", + "url" : "http://tokbox.com.archive2.s3.amazonaws.com/123456%2Ff6e7ee58-d6cf-4a59-896b-6d56b158ec71%2Farchive.mp4?Expires=1395194362&AWSAccessKeyId=AKIAI6LQCPIXYVWCQV6Q&Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "notarealproperty" : "not a real value" + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Archives/changes_the_layout_of_an_archive.yml b/spec/cassettes/OpenTok_Archives/changes_the_layout_of_an_archive.yml new file mode 100644 index 00000000..2dc0daac --- /dev/null +++ b/spec/cassettes/OpenTok_Archives/changes_the_layout_of_an_archive.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: put + uri: https://api.opentok.com/v2/project/123456/archive/30b3ebf1-ba36-4f5b-8def-6f70d9986fe9/layout + body: + encoding: US-ASCII + string: '{"type":"pip"}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 26 Sep 2018 18:22:36 GMT + Content-Type: + - application/json + Connection: + - keep-alive + Content-Length: + - '73' + body: + encoding: UTF-8 + string: '{"code":-1,"message":"Issuer not found","description":"Issuer not found"}' + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Archives/should_create_hd_archives.yml b/spec/cassettes/OpenTok_Archives/should_create_hd_archives.yml new file mode 100644 index 00000000..64f8791f --- /dev/null +++ b/spec/cassettes/OpenTok_Archives/should_create_hd_archives.yml @@ -0,0 +1,52 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/archive + body: + encoding: UTF-8 + string: '{"sessionId":"SESSIONID","outputMode":"composed","resolution":"1280x720"}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Fri, 21 Sep 2018 16:54:25 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + body: + encoding: UTF-8 + string: |- + { + "createdAt" : 1395193762293, + "duration" : 0, + "id" : "d7f4d2a3-da74-414d-868a-190532a835bc", + "name" : "ARCHIVE NAME", + "partnerId" : 123456, + "reason" : "", + "sessionId" : "SESSIONID", + "size" : 0, + "status" : "started", + "url" : null, + "hasAudio" : true, + "hasVideo" : false, + "outputMode": "composed", + "resolution": "1280x720" + } + http_version: + recorded_at: Fri, 21 Sep 2018 16:54:25 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Broadcasts/calls_layout_on_broadcast_object.yml b/spec/cassettes/OpenTok_Broadcasts/calls_layout_on_broadcast_object.yml new file mode 100644 index 00000000..cebde193 --- /dev/null +++ b/spec/cassettes/OpenTok_Broadcasts/calls_layout_on_broadcast_object.yml @@ -0,0 +1,55 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.opentok.com/v2/project/123456/broadcast/13dbcc23-af92-4862-9184-74b21815a814 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Fri, 28 Sep 2018 20:45:36 GMT + Content-Type: + - application/json + Connection: + - keep-alive + body: + encoding: UTF-8 + string: |- + { + "id":"13dbcc23-af92-4862-9184-74b21815a814", + "sessionId":"SESSIONID", + "projectId":123456, + "createdAt":1538160235541, + "broadcastUrls":{ + "rtmp":[ + { + "status":"live", + "id":"rubyTestStream", + "serverUrl":"rtmp://x.rtmp.youtube.com/live2", + "streamName":"66c9-jwuh-pquf-9x18" + } + ], + "hls":"https://cdn-broadcast001-pdx.tokbox.com/14935/14935_77e4e8e8-0c8b-4cea-b579-0560875f7123.smil/playlist.m3u8" + }, + "updatedAt":1538160235541, + "status":"started", + "maxDuration":7200, + "resolution":"640x480", + "partnerId":123456, + "event":"broadcast" + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Broadcasts/changes_the_layout_of_a_broadcast.yml b/spec/cassettes/OpenTok_Broadcasts/changes_the_layout_of_a_broadcast.yml new file mode 100644 index 00000000..91904927 --- /dev/null +++ b/spec/cassettes/OpenTok_Broadcasts/changes_the_layout_of_a_broadcast.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: put + uri: https://api.opentok.com/v2/project/123456/broadcast/13dbcc23-af92-4862-9184-74b21815a814/layout + body: + encoding: UTF-8 + string: '{"type":"verticalPresentation"}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Fri, 28 Sep 2018 20:40:56 GMT + Content-Type: + - application/json + Connection: + - keep-alive + Content-Length: + - '73' + body: + encoding: UTF-8 + string: '{"code":-1,"message":"Issuer not found","description":"Issuer not found"}' + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Broadcasts/fetches_a_hls_broadcast_url.yml b/spec/cassettes/OpenTok_Broadcasts/fetches_a_hls_broadcast_url.yml new file mode 100644 index 00000000..60c71319 --- /dev/null +++ b/spec/cassettes/OpenTok_Broadcasts/fetches_a_hls_broadcast_url.yml @@ -0,0 +1,50 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/broadcast + body: + encoding: UTF-8 + string: '{"sessionId":"SESSIONID","outputs":{"hls":{}}}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 27 Sep 2018 22:34:34 GMT + Content-Type: + - application/json + Connection: + - keep-alive + body: + encoding: UTF-8 + string: |- + { + "id":"BROADCASTID", + "sessionId":"SESSIONID", + "projectId":123456, + "createdAt":1538086900154, + "broadcastUrls": + { + "hls":"https://cdn-broadcast001-pdx.tokbox.com/14787/14787_b930bf08-1c9f-4c55-ab04-7d192578c057.smil/playlist.m3u8" + }, + "updatedAt":1538086900489, + "status":"started", + "maxDuration":7200, + "resolution":"640x480", + "partnerId":100, + "event":"broadcast" + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Broadcasts/finds_a_broadcast.yml b/spec/cassettes/OpenTok_Broadcasts/finds_a_broadcast.yml new file mode 100644 index 00000000..b28e772b --- /dev/null +++ b/spec/cassettes/OpenTok_Broadcasts/finds_a_broadcast.yml @@ -0,0 +1,55 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.opentok.com/v2/project/123456/broadcast/13dbcc23-af92-4862-9184-74b21815a814 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Fri, 28 Sep 2018 18:46:21 GMT + Content-Type: + - application/json + Connection: + - keep-alive + body: + encoding: UTF-8 + string: |- + { + "id":"13dbcc23-af92-4862-9184-74b21815a814", + "sessionId":"SESSIONID", + "projectId":123456, + "createdAt":1538160235541, + "broadcastUrls":{ + "rtmp":[ + { + "status":"live", + "id":"rubyTestStream", + "serverUrl":"rtmp://x.rtmp.youtube.com/live2", + "streamName":"66c9-jwuh-pquf-9x18" + } + ], + "hls":"https://cdn-broadcast001-pdx.tokbox.com/14935/14935_77e4e8e8-0c8b-4cea-b579-0560875f7123.smil/playlist.m3u8" + }, + "updatedAt":1538160235541, + "status":"started", + "maxDuration":7200, + "resolution":"640x480", + "partnerId":123456, + "event":"broadcast" + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Broadcasts/starts_a_rtmp_broadcast.yml b/spec/cassettes/OpenTok_Broadcasts/starts_a_rtmp_broadcast.yml new file mode 100644 index 00000000..8fa21f56 --- /dev/null +++ b/spec/cassettes/OpenTok_Broadcasts/starts_a_rtmp_broadcast.yml @@ -0,0 +1,59 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/broadcast + body: + encoding: UTF-8 + string: '{"sessionId":"SESSIONID","outputs":{"hls":{},"rtmp":[{"id":"rubyTestStream","serverUrl":"rtmp://x.rtmp.youtube.com/live2","streamName":"66c9-jwuh-pquf-9x18"}]}}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 27 Sep 2018 23:10:28 GMT + Content-Type: + - application/json + Connection: + - keep-alive + body: + encoding: UTF-8 + string: |- + { + "id":"BROADCASTID", + "sessionId":"SESSIONID", + "projectId":123456, + "createdAt":1538086900154, + "broadcastUrls": + { + "hls":"https://cdn-broadcast001-pdx.tokbox.com/14787/14787_b930bf08-1c9f-4c55-ab04-7d192578c057.smil/playlist.m3u8", + "rtmp": + [ + { + "status":"connecting", + "id":"rubyTestStream", + "serverUrl":"rtmp://x.rtmp.youtube.com/live2", + "streamName":"66c9-jwuh-pquf-9x18" + } + ] + }, + "updatedAt":1538086900489, + "status":"started", + "maxDuration":7200, + "resolution":"640x480", + "partnerId":100, + "event":"broadcast" + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Broadcasts/stops_a_broadcast.yml b/spec/cassettes/OpenTok_Broadcasts/stops_a_broadcast.yml new file mode 100644 index 00000000..32ce6553 --- /dev/null +++ b/spec/cassettes/OpenTok_Broadcasts/stops_a_broadcast.yml @@ -0,0 +1,45 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/broadcast/13dbcc23-af92-4862-9184-74b21815a814/stop + body: + encoding: UTF-8 + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Fri, 28 Sep 2018 19:23:21 GMT + Content-Type: + - application/json + Connection: + - keep-alive + body: + encoding: UTF-8 + string: |- + { + "id":"13dbcc23-af92-4862-9184-74b21815a814", + "sessionId":"SESSIONID", + "projectId":123456, + "createdAt":1538162113387, + "broadcastUrls":null, + "updatedAt":1538162113387, + "status":"stopped", + "maxDuration":7200, + "resolution":"640x480", + "event":"broadcast", + "partnerId":123456 + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Connections/forces_a_connection_to_be_terminated.yml b/spec/cassettes/OpenTok_Connections/forces_a_connection_to_be_terminated.yml new file mode 100644 index 00000000..2933a195 --- /dev/null +++ b/spec/cassettes/OpenTok_Connections/forces_a_connection_to_be_terminated.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: delete + uri: https://api.opentok.com/v2/project/123456/session/SESSIONID/connection/CONNID + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 204 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 29 Aug 2018 19:41:26 GMT + Content-Type: + - application/json + Connection: + - keep-alive + Content-Length: + - '73' + body: + encoding: UTF-8 + string: '{"code":-1,"message":"Issuer not found","description":"Issuer not found"}' + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_a_connection.yml b/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_a_connection.yml new file mode 100644 index 00000000..fb8e75e2 --- /dev/null +++ b/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_a_connection.yml @@ -0,0 +1,37 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/session/SESSIONID/connection/CONNID/signal + body: + encoding: UTF-8 + string: '{"type":"chat","data":"Hello"}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 204 + message: OK + headers: + Server: + - nginx + Date: + - Tue, 28 Aug 2018 22:01:54 GMT + Content-Type: + - application/xml + Connection: + - keep-alive + Content-Length: + - '177' + body: + encoding: UTF-8 + string: -1Issuer + not foundIssuer not found + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_all_connections.yml b/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_all_connections.yml new file mode 100644 index 00000000..8e91caf9 --- /dev/null +++ b/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_all_connections.yml @@ -0,0 +1,37 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.opentok.com/v2/project/123456/session/SESSIONID/signal + body: + encoding: UTF-8 + string: '{"type":"chat","data":"Hello"}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 204 + message: OK + headers: + Server: + - nginx + Date: + - Tue, 28 Aug 2018 21:31:02 GMT + Content-Type: + - application/xml + Connection: + - keep-alive + Content-Length: + - '177' + body: + encoding: UTF-8 + string: -1Issuer + not foundIssuer not found + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Streams/get_all_streams_information.yml b/spec/cassettes/OpenTok_Streams/get_all_streams_information.yml new file mode 100644 index 00000000..857c9b49 --- /dev/null +++ b/spec/cassettes/OpenTok_Streams/get_all_streams_information.yml @@ -0,0 +1,53 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.opentok.com/v2/project/123456/session/SESSIONID/stream + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 24 Sep 2018 20:41:24 GMT + Content-Type: + - application/json + Connection: + - keep-alive + Content-Length: + - '73' + body: + encoding: UTF-8 + string: |- + { + "count": 2, + "items": [ + { + "id": "1234356-0a06-46a2-8ea8-074e64d43422", + "videoType": "camera", + "name": "", + "layoutClassList": ["full", "focus"] + }, + { + "id": "8b732909-0a06-46a2-8ea8-074e64d43422", + "videoType": "screen", + "name": "", + "layoutClassList": ["full"] + } + ] + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Streams/get_specific_stream_information.yml b/spec/cassettes/OpenTok_Streams/get_specific_stream_information.yml new file mode 100644 index 00000000..52cabd45 --- /dev/null +++ b/spec/cassettes/OpenTok_Streams/get_specific_stream_information.yml @@ -0,0 +1,42 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.opentok.com/v2/project/123456/session/SESSIONID/stream/STREAMID + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 24 Sep 2018 20:41:24 GMT + Content-Type: + - application/json + Connection: + - keep-alive + Content-Length: + - '0' + body: + encoding: UTF-8 + string: |- + { + "id": "8b732909-0a06-46a2-8ea8-074e64d43422", + "videoType": "camera", + "name": "", + "layoutClassList": ["full"] + } + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/cassettes/OpenTok_Streams/layout_working_on_two_stream_list.yml b/spec/cassettes/OpenTok_Streams/layout_working_on_two_stream_list.yml new file mode 100644 index 00000000..12069ad3 --- /dev/null +++ b/spec/cassettes/OpenTok_Streams/layout_working_on_two_stream_list.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: put + uri: https://api.opentok.com/v2/project/123456/session/SESSIONID/stream + body: + encoding: UTF-8 + string: '{"items":[{"id":"8b732909-0a06-46a2-8ea8-074e64d43422","layoutClassList":["full"]},{"id":"8b732909-0a06-46a2-8ea8-074e64d43423","layoutClassList":["full","focus"]}]}' + headers: + User-Agent: + - OpenTok-Ruby-SDK/<%= version %> + X-Opentok-Auth: + - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzdCI6InByb2plY3QifQ.eyJpc3MiOiIxMjM0NTYiLCJpYXQiOjE0OTI1MTA2NjAsImV4cCI6MTQ5MjUxMDk2MH0.Oh_JHhtEUKK1pPV4s6neXJj_RXI8EcEpJRRpG_2c9U0 + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 26 Sep 2018 22:48:29 GMT + Content-Type: + - application/json + Connection: + - keep-alive + Content-Length: + - '73' + body: + encoding: UTF-8 + string: '{"code":-1,"message":"Issuer not found","description":"Issuer not found"}' + http_version: + recorded_at: Tue, 18 Apr 2017 10:17:40 GMT +recorded_with: VCR 2.8.0 diff --git a/spec/opentok/archives_spec.rb b/spec/opentok/archives_spec.rb index ef54b6b3..89c5d1a6 100644 --- a/spec/opentok/archives_spec.rb +++ b/spec/opentok/archives_spec.rb @@ -93,6 +93,79 @@ expect(archive).to be_an_instance_of OpenTok::Archive end + it "should create hd archives", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" } } do + archive = archives.create session_id, { :output_mode => :composed, :resolution => "1280x720" } + expect(archive).to be_an_instance_of OpenTok::Archive + expect(archive.session_id).to eq session_id + expect(archive.output_mode).to eq :composed + expect(archive.resolution).to eq "1280x720" + end + + it "should create archives throws exception", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" } } do + opts = { + :output_mode => :individual, + :resolution => "1280x720" + } + + expect { archives.create session_id, opts }.to raise_exception + end + + it "raise an error if layout options are empty" do + expect { + archives.layout(started_archive_id, {}) + }.to raise_error(ArgumentError) + end + + it "raise an error if archive id is not provided" do + expect { + archives.layout("", { + type: "custom", + stylesheet: "the layout stylesheet (only used with type == custom)" + }) + }.to raise_error(ArgumentError) + end + + it "raise an error if custom type has no style sheet" do + expect { + archives.layout(started_archive_id, { + type: "custom", + }) + }.to raise_error(ArgumentError) + end + + it "raise an error if non-custom type has style sheet" do + expect { + archives.layout(started_archive_id, { + type: "pip", + stylesheet: "the layout stylesheet (only used with type == custom)" + }) + }.to raise_error(ArgumentError) + end + + it "raise an error if invalid layout type" do + expect { + archives.layout(started_archive_id, { + type: "pip1" + }) + }.to raise_error(ArgumentError) + end + it "calls layout on archive object", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" } } do + archive = archives.find findable_archive_id + expect(archive).to be_an_instance_of OpenTok::Archive + expect(archive.id).to eq findable_archive_id + expect { + archive.layout( + type: 'pip1', + ) + }.to raise_error(ArgumentError) + end + it "changes the layout of an archive", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" } } do + response = archives.layout(started_archive_id, { + type: "pip" + }) + expect(response).not_to be_nil + end + # TODO: context "with a session that has no participants" do # let(:session_id) { "" } # it "should refuse to create archives with appropriate error" do diff --git a/spec/opentok/broadcasts_spec.rb b/spec/opentok/broadcasts_spec.rb new file mode 100644 index 00000000..1ca75163 --- /dev/null +++ b/spec/opentok/broadcasts_spec.rb @@ -0,0 +1,171 @@ +require "opentok/broadcast" +require "opentok/broadcasts" +require "opentok/opentok" +require "opentok/version" +require "spec_helper" + +describe OpenTok::Broadcasts 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(:broadcast_id) { "BROADCASTID" } + let(:started_broadcast_id) { "13dbcc23-af92-4862-9184-74b21815a814" } + let(:opentok) { OpenTok::OpenTok.new api_key, api_secret } + let(:broadcast) { opentok.broadcasts } + + subject { broadcast } + + it 'raise an error on empty sessionId' do + opts = { + :outputs => { + :hls => {} + } + } + expect { + broadcast.create('', opts) + }.to raise_error(ArgumentError) + end + it 'raise an error on nil sessionId' do + opts = { + :outputs => { + :hls => {} + } + } + expect { + broadcast.create(nil, opts) + }.to raise_error(ArgumentError) + end + it 'raise an error on empty options' do + expect { + broadcast.create(nil, {}) + }.to raise_error(ArgumentError) + end + it 'fetches a hls broadcast url', :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + opts = { + :outputs => { + :hls => {} + } + } + b_hls = broadcast.create(session_id, opts) + expect(b_hls).to be_an_instance_of OpenTok::Broadcast + expect(b_hls.id).to eq broadcast_id + expect(b_hls.broadcastUrls['hls']).to eq "https://cdn-broadcast001-pdx.tokbox.com/14787/14787_b930bf08-1c9f-4c55-ab04-7d192578c057.smil/playlist.m3u8" + end + + it 'starts a rtmp broadcast', :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + opts = { + :outputs => { + :hls => {}, + :rtmp => [ + { + :id => "rubyTestStream", + :serverUrl => "rtmp://x.rtmp.youtube.com/live2", + :streamName => "66c9-jwuh-pquf-9x18" + } + ] + } + } + b_rtmp = broadcast.create(session_id, opts) + expect(b_rtmp).to be_an_instance_of OpenTok::Broadcast + expect(b_rtmp.id).to eq broadcast_id + expect(b_rtmp.broadcastUrls["rtmp"][0]["serverUrl"]).to eq "rtmp://x.rtmp.youtube.com/live2" + expect(b_rtmp.broadcastUrls["rtmp"].count).to eq 1 + end + + it 'finds a broadcast', :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + b = broadcast.find started_broadcast_id + expect(b).to be_an_instance_of OpenTok::Broadcast + expect(b.id).to eq started_broadcast_id + expect(b.broadcastUrls["rtmp"][0]["serverUrl"]).to eq "rtmp://x.rtmp.youtube.com/live2" + expect(b.broadcastUrls["rtmp"].count).to eq 1 + end + it 'raise an error on empty broadcastId in find' do + expect { + broadcast.find("") + }.to raise_error(ArgumentError) + end + it 'raise an error on nil broadcastId in find' do + expect { + broadcast.find(nil) + }.to raise_error(ArgumentError) + end + it 'raise an error on empty broadcastId stop' do + expect { + broadcast.stop("") + }.to raise_error(ArgumentError) + end + it 'raise an error on nil broadcastId stop' do + expect { + broadcast.stop(nil) + }.to raise_error(ArgumentError) + end + it 'stops a broadcast', :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + b = broadcast.stop(started_broadcast_id) + expect(b).to be_an_instance_of OpenTok::Broadcast + expect(b.id).to eq started_broadcast_id + expect(b.broadcastUrls).to be_nil + expect(b.status).to eq "stopped" + end + + it "raise an error if layout options are empty" do + expect { + broadcast.layout(started_broadcast_id, {}) + }.to raise_error(ArgumentError) + end + + it "raise an error if broadcast id is not provided" do + expect { + broadcast.layout("", { + type: "custom", + stylesheet: "the layout stylesheet (only used with type == custom)" + }) + }.to raise_error(ArgumentError) + end + + it "raise an error if custom type has no style sheet" do + expect { + broadcast.layout(started_broadcast_id, { + type: "custom", + }) + }.to raise_error(ArgumentError) + end + + it "raise an error if non-custom type has style sheet" do + expect { + broadcast.layout(started_broadcast_id, { + type: "pip", + stylesheet: "the layout stylesheet (only used with type == custom)" + }) + }.to raise_error(ArgumentError) + end + + it "raise an error if invalid layout type" do + expect { + broadcast.layout(started_broadcast_id, { + type: "pip1" + }) + }.to raise_error(ArgumentError) + end + it "calls layout on broadcast object", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" } } do + b = broadcast.find started_broadcast_id + expect(b).to be_an_instance_of OpenTok::Broadcast + expect(b.id).to eq started_broadcast_id + expect { + b.layout( + :type => 'pip1', + ) + }.to raise_error(ArgumentError) + end + it "changes the layout of a broadcast", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" } } do + response = broadcast.layout(started_broadcast_id, { + :type => "verticalPresentation" + }) + expect(response).not_to be_nil + end + +end \ No newline at end of file diff --git a/spec/opentok/connection_spec.rb b/spec/opentok/connection_spec.rb new file mode 100644 index 00000000..3d682436 --- /dev/null +++ b/spec/opentok/connection_spec.rb @@ -0,0 +1,38 @@ +require "opentok/opentok" +require "opentok/version" +require "opentok/connections" +require "spec_helper" + +describe OpenTok::Connections 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(:connection_id) { "CONNID" } + let(:opentok) { OpenTok::OpenTok.new api_key, api_secret } + let(:connection) { opentok.connections } + + subject { connection } + + + it 'raise an error on nil session_id' do + expect { + connection.forceDisconnect(nil,connection_id) + }.to raise_error(ArgumentError) + end + + it 'raise an error on nil connection_id' do + expect { + connection.forceDisconnect(session_id,nil) + }.to raise_error(ArgumentError) + end + + it "forces a connection to be terminated", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + response = connection.forceDisconnect(session_id, connection_id) + expect(response).not_to be_nil + end +end \ No newline at end of file diff --git a/spec/opentok/signal_spec.rb b/spec/opentok/signal_spec.rb new file mode 100644 index 00000000..12d0a875 --- /dev/null +++ b/spec/opentok/signal_spec.rb @@ -0,0 +1,50 @@ +require "opentok/signals" +require "opentok/opentok" +require "opentok/version" +require "spec_helper" + +describe OpenTok::Signals 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(:connection_id) { "CONNID" } + let(:opentok) { OpenTok::OpenTok.new api_key, api_secret } + let(:signal) { opentok.signals } + + subject { signal } + + it 'raise an error on nil sessionId' do + expect { + signal.send(nil) + }.to raise_error(ArgumentError) + end + + it 'raise an error on empty sessionId' do + expect { + signal.send('') + }.to raise_error(ArgumentError) + end + + it "receives a valid response for all connections", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + opts = { "type" => "chat", + "data" => "Hello", + } + response = signal.send(session_id, "", opts) + expect(response).not_to be_nil + end + + it "receives a valid response for a connection", :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + opts = { "type" => "chat", + "data" => "Hello", + } + response = signal.send(session_id, connection_id, opts) + expect(response).not_to be_nil + end + + +end \ No newline at end of file diff --git a/spec/opentok/streams_spec.rb b/spec/opentok/streams_spec.rb new file mode 100644 index 00000000..3fc45451 --- /dev/null +++ b/spec/opentok/streams_spec.rb @@ -0,0 +1,75 @@ +require 'opentok/opentok' +require 'opentok/version' +require 'opentok/streams' +require 'opentok/stream' +require 'opentok/stream_list' + +require 'spec_helper' + +describe OpenTok::Streams 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(:stream_id) { 'STREAMID' } + let(:opentok) { OpenTok::OpenTok.new api_key, api_secret } + let(:streams) { opentok.streams} + + subject { streams } + + it { should be_an_instance_of OpenTok::Streams } + it 'raise an error on nil session_id' do + expect { + streams.all(nil) + }.to raise_error(ArgumentError) + end + it 'raise an error on empty session_id' do + expect { + streams.all('') + }.to raise_error(ArgumentError) + end + it 'get all streams information', :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + all_streams = streams.all(session_id) + expect(all_streams).to be_an_instance_of OpenTok::StreamList + expect(all_streams.total).to eq 2 + expect(all_streams[0].layoutClassList[1]).to eq "focus" + end + it 'get specific stream information', :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + stream = streams.find(session_id, stream_id) + expect(stream).to be_an_instance_of OpenTok::Stream + expect(stream.videoType).to eq 'camera' + expect(stream.layoutClassList.count).to eq 1 + expect(stream.layoutClassList.first).to eq "full" + expect(stream.id).not_to be_nil + end + it 'layout raises an error on empty session_id' do + expect { + streams.layout('', {} ) + }.to raise_error(ArgumentError) + end + it 'layout raises an error on an empty stream list' do + expect { + streams.layout(session_id, {}) + }.to raise_error(ArgumentError) + end + it 'layout working on two stream list', :vcr => { :erb => { :version => OpenTok::VERSION + "-Ruby-Version-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"} } do + streams_list = { + :items => [ + { + :id => "8b732909-0a06-46a2-8ea8-074e64d43422", + :layoutClassList => ["full"] + }, + { + :id => "8b732909-0a06-46a2-8ea8-074e64d43423", + :layoutClassList => ["full", "focus"] + } + ] + } + response = streams.layout(session_id, streams_list) + expect(response).not_to be_nil + end +end \ No newline at end of file