Repository: opentok/Opentok-Python-SDK

Issue:
OpenTok.start_archive() doesn't have parameter 'resolution'
OpenTok.start_archive() doesn't have parameter 'resolution'. Python SDK is simply through requests.post to use Opentok REST API and it doesn't pass the parameter 'resolution' in the payload json which is default of '640x480'.  

Discussion:
maikthomas: Updated with requested changes.
@msach22
maikthomas: @msach22 updated

Patch:
diff --git a/README.rst b/README.rst
index 3c51420..3b9d05a 100644
--- a/README.rst
+++ b/README.rst
@@ -45,14 +45,14 @@ Creating Sessions
 ~~~~~~~~~~~~~~~~~
 
 To create an OpenTok Session, use the ``opentok.create_session()`` method. There are three optional
-keyword parameters for this method: 
+keyword parameters for this method:
 
 * ``location`` which can be set to a string containing an IP address.
 
 * ``media_mode`` which is a String (defined by the MediaModes class).
   This determines whether the session will use the
   `OpenTok Media Router <https://tokbox.com/developer/guides/create-session/#media-mode>`_
-  or attempt to send streams directly between clients. A routed session is required for some 
+  or attempt to send streams directly between clients. A routed session is required for some
   OpenTok features (such as archiving).
 
 * ``archive_mode`` which specifies whether the session will be automatically archived (``always``)
@@ -142,6 +142,20 @@ streams in the session to individual files (instead of a single composed file) b
   # Store this archive_id in the database
   archive_id = archive.id
 
+Composed archives (output_mode=OutputModes.composed) have an optional ``resolution`` parameter.
+If no value is supplied the opentok platform will use the default resolution "640x480".
+You can set this to "1280x720" by setting the
+``resolution`` parameter of the ``opentok.start_archive()`` method.
+
+Warning: This value cannot be set for Individual output mode, an error will be thrown.
+
+.. code:: python
+
+  archive = opentok.start_archive(session_id, name=u'Important Presentation', resolution="1280x720")
+
+  # Store this archive_id in the database
+  archive_id = archive.id
+
 You can stop the recording of a started Archive using the ``opentok.stop_archive(archive_id)``
 method. You can also do this using the ``archive.stop()`` method of an ``Archive`` instance.
 
diff --git a/opentok/archives.py b/opentok/archives.py
index 123e4fd..36ac331 100644
--- a/opentok/archives.py
+++ b/opentok/archives.py
@@ -102,6 +102,7 @@ class Archive(object):
         self.has_video = values.get('hasVideo')
         self.output_mode = OutputModes[values.get('outputMode', 'composed')]
         self.url = values.get('url')
+        self.resolution = values.get('resolution')
 
     def stop(self):
         """
diff --git a/opentok/opentok.py b/opentok/opentok.py
index fe71593..ce22610 100644
--- a/opentok/opentok.py
+++ b/opentok/opentok.py
@@ -311,7 +311,7 @@ class OpenTok(object):
             url = url + '/' + archive_id
         return url
 
-    def start_archive(self, session_id, has_audio=True, has_video=True, name=None, output_mode=OutputModes.composed):
+    def start_archive(self, session_id, has_audio=True, has_video=True, name=None, output_mode=OutputModes.composed, resolution=None):
         """
         Starts archiving an OpenTok session.
 
@@ -338,6 +338,10 @@ class OpenTok(object):
         :param OutputModes output_mode: Whether all streams in the archive are recorded
           to a single file (OutputModes.composed, the default) or to individual files
           (OutputModes.individual).
+        :param String resolution (Optional): The resolution of the archive, either "640x480" (the default)
+          or "1280x720". This parameter only applies to composed archives. If you set this
+          parameter and set the output_mode parameter to OutputModes.individual, the call to the
+          start_archive() method results in an error.
 
         :rtype: The Archive object, which includes properties defining the archive,
           including the archive ID.
@@ -345,11 +349,16 @@ class OpenTok(object):
         if not isinstance(output_mode, OutputModes):
             raise OpenTokException(u('Cannot start archive, {0} is not a valid output mode').format(output_mode))
 
+
+        if resolution and output_mode == OutputModes.individual:
+            raise OpenTokException(u('Invalid parameters: Resolution cannot be supplied for individual output mode.'))
+
         payload = {'name': name,
                    'sessionId': session_id,
                    'hasAudio': has_audio,
                    'hasVideo': has_video,
-                   'outputMode': output_mode.value
+                   'outputMode': output_mode.value,
+                   'resolution': resolution,
         }
 
         response = requests.post(self.archive_url(), data=json.dumps(payload), headers=self.archive_headers(), proxies=self.proxies, timeout=self.timeout)
@@ -359,7 +368,14 @@ class OpenTok(object):
         elif response.status_code == 403:
             raise AuthError()
         elif response.status_code == 400:
-            raise RequestError("Session ID is invalid")
+            """
+            The HTTP response has a 400 status code in the following cases:
+            You do not pass in a session ID or you pass in an invalid session ID.
+            No clients are actively connected to the OpenTok session.
+            You specify an invalid resolution value.
+            The outputMode property is set to "individual" and you set the resolution property and (which is not supported in individual stream archives).
+            """
+            raise RequestError(response.json().get("message"))
         elif response.status_code == 404:
             raise NotFoundError("Session not found")
         elif response.status_code == 409:
diff --git a/sample/Archiving/archiving.py b/sample/Archiving/archiving.py
index bcc8d76..c06775f 100644
--- a/sample/Archiving/archiving.py
+++ b/sample/Archiving/archiving.py
@@ -57,8 +57,10 @@ def start():
     has_audio = 'hasAudio' in request.form.keys()
     has_video = 'hasVideo' in request.form.keys()
     output_mode = OutputModes[request.form.get('outputMode')]
+    resolution = request.form.get('resolution')
     archive = opentok.start_archive(session.session_id, name="Python Archiving Sample App",
-                                    has_audio=has_audio, has_video=has_video, output_mode=output_mode)
+                                    has_audio=has_audio, has_video=has_video,
+                                    output_mode=output_mode, resolution=resolution)
     return archive.json()
 
 @app.route("/stop/<archive_id>")
diff --git a/sample/Archiving/static/js/host.js b/sample/Archiving/static/js/host.js
index 4cb650f..c60e4fb 100644
--- a/sample/Archiving/static/js/host.js
+++ b/sample/Archiving/static/js/host.js
@@ -40,10 +40,32 @@ session.on('archiveStopped', function(event) {
   enableForm();
 });
 
+function addModeRadioOnClicks() {
+  var toggleResolution = function(enable) {
+    return function () {
+      var resolutionFieldset = $('#resolution-fieldset');
+      if (enable) {
+        resolutionFieldset.removeAttr('disabled');
+        resolutionFieldset.find(':input').removeAttr('disabled')
+      } else {
+        resolutionFieldset.attr('disabled', 'disabled')
+        resolutionFieldset.find(':input').attr('disabled', 'disabled')
+      }
+    }
+  }
+
+  $('#composedModeRadio').click(toggleResolution(true));
+  $('#individualModeRadio').click(toggleResolution(false));
+
+}
+
 $(document).ready(function() {
+  addModeRadioOnClicks()
+
   $('.start').click(function (event) {
     var options = $('.archive-options').serialize();
     disableForm();
+
     $.post('/start', options).fail(enableForm);
   }).show();
   $('.stop').click(function(event){
diff --git a/sample/Archiving/templates/host.html b/sample/Archiving/templates/host.html
index a2eb108..c8dd507 100644
--- a/sample/Archiving/templates/host.html
+++ b/sample/Archiving/templates/host.html
@@ -30,12 +30,22 @@
                   <div class="form-group">
                       <p class="help-block">Output Mode:</p>
                       <label class="radio-inline">
-                          <input type="radio" name="outputMode" value="composed" checked> Composed
+                          <input type="radio" id="composedModeRadio" name="outputMode" value="composed" checked> Composed
                       </label>
                       <label class="radio-inline">
-                          <input type="radio" name="outputMode" value="individual"> Individual
+                          <input type="radio" id="individualModeRadio" name="outputMode" value="individual"> Individual
                       </label>
                   </div>
+
+                  <fieldset id="resolution-fieldset" class="form-group">
+                    <p class="help-block">Resolution</p>
+                    <label class="radio-inline">
+                        <input type="radio" name="resolution" value="640x480" checked> 640x480
+                    </label>
+                    <label class="radio-inline">
+                        <input type="radio" name="resolution" value="1280x720"> 1280x720
+                    </label>
+                </fieldset>
               </fieldset>
           </form>
           <button class="btn btn-danger start">Start archiving</button>


Test patch:
diff --git a/tests/test_archive_api.py b/tests/test_archive_api.py
index 6902209..d90bf40 100644
--- a/tests/test_archive_api.py
+++ b/tests/test_archive_api.py
@@ -9,7 +9,7 @@ import datetime
 import pytz
 from .validate_jwt import validate_jwt_header
 
-from opentok import OpenTok, Archive, ArchiveList, OutputModes, __version__
+from opentok import OpenTok, Archive, ArchiveList, OutputModes, OpenTokException, __version__
 
 class OpenTokArchiveApiTest(unittest.TestCase):
     def setUp(self):
@@ -118,6 +118,119 @@ class OpenTokArchiveApiTest(unittest.TestCase):
         expect(archive).to(have_property(u('duration'), equal(0)))
         expect(archive).to(have_property(u('url'), equal(None)))
 
+    @httpretty.activate
+    def test_start_archive_with_640x480_resolution(self):
+        httpretty.register_uri(httpretty.POST, u('https://api.opentok.com/v2/project/{0}/archive').format(self.api_key),
+                               body=textwrap.dedent(u("""\
+                                       {
+                                          "createdAt" : 1395183243556,
+                                          "duration" : 0,
+                                          "id" : "30b3ebf1-ba36-4f5b-8def-6f70d9986fe9",
+                                          "name" : "ARCHIVE NAME",
+                                          "partnerId" : 123456,
+                                          "reason" : "",
+                                          "sessionId" : "SESSIONID",
+                                          "size" : 0,
+                                          "status" : "started",
+                                          "hasAudio": true,
+                                          "hasVideo": true,
+                                          "outputMode": "composed",
+                                          "url" : null,
+                                          "resolution": "640x480"
+                                        }""")),
+                               status=200,
+                               content_type=u('application/json'))
+
+        archive = self.opentok.start_archive(self.session_id, resolution="640x480")
+
+        validate_jwt_header(self, httpretty.last_request().headers[u('x-opentok-auth')])
+        expect(httpretty.last_request().headers[u('user-agent')]).to(contain(u('OpenTok-Python-SDK/')+__version__))
+        expect(httpretty.last_request().headers[u('content-type')]).to(equal(u('application/json')))
+        # non-deterministic json encoding. have to decode to test it properly
+        if PY2:
+            body = json.loads(httpretty.last_request().body)
+        if PY3:
+            body = json.loads(httpretty.last_request().body.decode('utf-8'))
+        expect(body).to(have_key(u('sessionId'), u('SESSIONID')))
+        expect(body).to(have_key(u('resolution'), u('640x480')))
+        expect(archive).to(be_an(Archive))
+        expect(archive).to(have_property(u('id'), u('30b3ebf1-ba36-4f5b-8def-6f70d9986fe9')))
+        expect(archive).to(have_property(u('resolution'), "640x480"))
+        expect(archive).to(have_property(u('status'), u('started')))
+        expect(archive).to(have_property(u('session_id'), u('SESSIONID')))
+        expect(archive).to(have_property(u('partner_id'), 123456))
+        if PY2:
+            created_at = datetime.datetime.fromtimestamp(1395183243, pytz.UTC)
+        if PY3:
+            created_at = datetime.datetime.fromtimestamp(1395183243, datetime.timezone.utc)
+        expect(archive).to(have_property(u('created_at'), equal(created_at)))
+        expect(archive).to(have_property(u('size'), equal(0)))
+        expect(archive).to(have_property(u('duration'), equal(0)))
+        expect(archive).to(have_property(u('url'), equal(None)))
+
+    @httpretty.activate
+    def test_start_archive_with_1280x720_resolution(self):
+        httpretty.register_uri(httpretty.POST, u('https://api.opentok.com/v2/project/{0}/archive').format(self.api_key),
+                               body=textwrap.dedent(u("""\
+                                       {
+                                          "createdAt" : 1395183243556,
+                                          "duration" : 0,
+                                          "id" : "30b3ebf1-ba36-4f5b-8def-6f70d9986fe9",
+                                          "name" : "ARCHIVE NAME",
+                                          "partnerId" : 123456,
+                                          "reason" : "",
+                                          "sessionId" : "SESSIONID",
+                                          "size" : 0,
+                                          "status" : "started",
+                                          "hasAudio": true,
+                                          "hasVideo": true,
+                                          "outputMode": "composed",
+                                          "url" : null,
+                                          "resolution": "1280x720"
+                                        }""")),
+                               status=200,
+                               content_type=u('application/json'))
+
+        archive = self.opentok.start_archive(self.session_id, resolution="1280x720")
+
+        validate_jwt_header(self, httpretty.last_request().headers[u('x-opentok-auth')])
+        expect(httpretty.last_request().headers[u('user-agent')]).to(contain(u('OpenTok-Python-SDK/')+__version__))
+        expect(httpretty.last_request().headers[u('content-type')]).to(equal(u('application/json')))
+        # non-deterministic json encoding. have to decode to test it properly
+        if PY2:
+            body = json.loads(httpretty.last_request().body)
+        if PY3:
+            body = json.loads(httpretty.last_request().body.decode('utf-8'))
+        expect(body).to(have_key(u('sessionId'), u('SESSIONID')))
+        expect(body).to(have_key(u('resolution'), u('1280x720')))
+        expect(archive).to(be_an(Archive))
+        expect(archive).to(have_property(u('id'), u('30b3ebf1-ba36-4f5b-8def-6f70d9986fe9')))
+        expect(archive).to(have_property(u('resolution'), "1280x720"))
+        expect(archive).to(have_property(u('status'), u('started')))
+        expect(archive).to(have_property(u('session_id'), u('SESSIONID')))
+        expect(archive).to(have_property(u('partner_id'), 123456))
+        if PY2:
+            created_at = datetime.datetime.fromtimestamp(1395183243, pytz.UTC)
+        if PY3:
+            created_at = datetime.datetime.fromtimestamp(1395183243, datetime.timezone.utc)
+        expect(archive).to(have_property(u('created_at'), equal(created_at)))
+        expect(archive).to(have_property(u('size'), equal(0)))
+        expect(archive).to(have_property(u('duration'), equal(0)))
+        expect(archive).to(have_property(u('url'), equal(None)))
+
+    def test_start_archive_individual_and_resolution_throws_error(self):
+        self.assertRaises(OpenTokException,
+            self.opentok.start_archive,
+            session_id=self.session_id,
+            output_mode=OutputModes.individual,
+            resolution="640x480")
+
+        self.assertRaises(OpenTokException,
+            self.opentok.start_archive,
+            session_id=self.session_id,
+            output_mode=OutputModes.individual,
+            resolution="1280x720")
+
     @httpretty.activate
     def test_start_voice_archive(self):
         httpretty.register_uri(httpretty.POST, u('https://api.opentok.com/v2/project/{0}/archive').format(self.api_key),