Bug 1106941 - Part 2: Firefox Hello doesn't work properly when no video camera is installed - fix incoming conversations. r=mikedeboer, a=lsblakk
authorMark Banner <standard8@mozilla.com>
Mon, 09 Mar 2015 23:42:08 +0000
changeset 250358 9e52698fd237
parent 250357 245598f10dcd
child 250359 19ac18d33c28
push id4557
push usermbanner@mozilla.com
push date2015-03-11 21:28 +0000
treeherdermozilla-beta@9e52698fd237 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer, lsblakk
bugs1106941
milestone37.0
Bug 1106941 - Part 2: Firefox Hello doesn't work properly when no video camera is installed - fix incoming conversations. r=mikedeboer, a=lsblakk
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/test/shared/views_test.js
browser/components/loop/test/standalone/webapp_test.js
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -63,16 +63,17 @@ loop.conversation = (function(mozL10n) {
 
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (React.createElement(IncomingConversationView, {
             client: this.props.client, 
             conversation: this.props.conversation, 
             sdk: this.props.sdk, 
+            isDesktop: true, 
             conversationAppStore: this.props.conversationAppStore, 
             feedbackStore: this.props.feedbackStore}
           ));
         }
         case "outgoing": {
           return (React.createElement(OutgoingConversationView, {
             store: this.props.conversationStore, 
             dispatcher: this.props.dispatcher, 
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -63,16 +63,17 @@ loop.conversation = (function(mozL10n) {
 
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (<IncomingConversationView
             client={this.props.client}
             conversation={this.props.conversation}
             sdk={this.props.sdk}
+            isDesktop={true}
             conversationAppStore={this.props.conversationAppStore}
             feedbackStore={this.props.feedbackStore}
           />);
         }
         case "outgoing": {
           return (<OutgoingConversationView
             store={this.props.conversationStore}
             dispatcher={this.props.dispatcher}
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -340,22 +340,29 @@ loop.conversationViews = (function(mozL1
   var IncomingConversationView = React.createClass({displayName: "IncomingConversationView",
     mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
 
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
+      isDesktop: React.PropTypes.bool,
       conversationAppStore: React.PropTypes.instanceOf(
         loop.store.ConversationAppStore).isRequired,
       feedbackStore:
         React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
     },
 
+    getDefaultProps: function() {
+      return {
+        isDesktop: false
+      };
+    },
+
     getInitialState: function() {
       return {
         callFailed: false, // XXX this should be removed when bug 1047410 lands.
         callStatus: "start"
       };
     },
 
     componentDidMount: function() {
@@ -398,16 +405,17 @@ loop.conversationViews = (function(mozL1
         }
         case "connected": {
           document.title = this.props.conversation.getCallIdentifier();
 
           var callType = this.props.conversation.get("selectedCallType");
 
           return (
             React.createElement(sharedViews.ConversationView, {
+              isDesktop: this.props.isDesktop, 
               initiate: true, 
               sdk: this.props.sdk, 
               model: this.props.conversation, 
               video: {enabled: callType !== "audio"}}
             )
           );
         }
         case "end": {
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -340,22 +340,29 @@ loop.conversationViews = (function(mozL1
   var IncomingConversationView = React.createClass({
     mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
 
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
+      isDesktop: React.PropTypes.bool,
       conversationAppStore: React.PropTypes.instanceOf(
         loop.store.ConversationAppStore).isRequired,
       feedbackStore:
         React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
     },
 
+    getDefaultProps: function() {
+      return {
+        isDesktop: false
+      };
+    },
+
     getInitialState: function() {
       return {
         callFailed: false, // XXX this should be removed when bug 1047410 lands.
         callStatus: "start"
       };
     },
 
     componentDidMount: function() {
@@ -398,16 +405,17 @@ loop.conversationViews = (function(mozL1
         }
         case "connected": {
           document.title = this.props.conversation.getCallIdentifier();
 
           var callType = this.props.conversation.get("selectedCallType");
 
           return (
             <sharedViews.ConversationView
+              isDesktop={this.props.isDesktop}
               initiate={true}
               sdk={this.props.sdk}
               model={this.props.conversation}
               video={{enabled: callType !== "audio"}}
             />
           );
         }
         case "end": {
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -142,17 +142,18 @@ loop.shared.views = (function(_, OT, l10
    */
   var ConversationView = React.createClass({displayName: "ConversationView",
     mixins: [Backbone.Events, sharedMixins.AudioMixin],
 
     propTypes: {
       sdk: React.PropTypes.object.isRequired,
       video: React.PropTypes.object,
       audio: React.PropTypes.object,
-      initiate: React.PropTypes.bool
+      initiate: React.PropTypes.bool,
+      isDesktop: React.PropTypes.bool
     },
 
     // height set to 100%" to fix video layout on Google Chrome
     // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
     publisherConfig: {
       insertMode: "append",
       width: "100%",
       height: "100%",
@@ -162,16 +163,17 @@ loop.shared.views = (function(_, OT, l10
         nameDisplayMode: "off",
         videoDisabledDisplayMode: "off"
       }
     },
 
     getDefaultProps: function() {
       return {
         initiate: true,
+        isDesktop: false,
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
       return {
         video: this.props.video,
@@ -182,16 +184,33 @@ loop.shared.views = (function(_, OT, l10
     componentWillMount: function() {
       if (this.props.initiate) {
         this.publisherConfig.publishVideo = this.props.video.enabled;
       }
     },
 
     componentDidMount: function() {
       if (this.props.initiate) {
+        /**
+         * XXX This is a workaround for desktop machines that do not have a
+         * camera installed. As we don't yet have device enumeration, when
+         * we do, this can be removed (bug 1138851), and the sdk should handle it.
+         */
+        if (this.props.isDesktop &&
+            !window.MediaStreamTrack.getSources) {
+          // If there's no getSources function, the sdk defines its own and caches
+          // the result. So here we define the "normal" one which doesn't get cached, so
+          // we can change it later.
+          window.MediaStreamTrack.getSources = function(callback) {
+            callback([{kind: "audio"}, {kind: "video"}]);
+          };
+        }
+
+        this.listenTo(this.props.sdk, "exception", this._handleSdkException.bind(this));
+
         this.listenTo(this.props.model, "session:connected",
                                         this._onSessionConnected);
         this.listenTo(this.props.model, "session:stream-created",
                                         this._streamCreated);
         this.listenTo(this.props.model, ["session:peer-hungup",
                                          "session:network-disconnected",
                                          "session:ended"].join(" "),
                                          this.stopPublishing);
@@ -246,16 +265,45 @@ loop.shared.views = (function(_, OT, l10
      * @param  {StreamEvent} event
      */
     _streamCreated: function(event) {
       var incoming = this.getDOMNode().querySelector(".remote");
       this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
     },
 
     /**
+     * Handles the SDK Exception event.
+     *
+     * https://tokbox.com/opentok/libraries/client/js/reference/ExceptionEvent.html
+     *
+     * @param {ExceptionEvent} event
+     */
+    _handleSdkException: function(event) {
+      /**
+       * XXX This is a workaround for desktop machines that do not have a
+       * camera installed. As we don't yet have device enumeration, when
+       * we do, this can be removed (bug 1138851), and the sdk should handle it.
+       */
+      if (this.publisher &&
+          event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
+          event.message === "Unknown Error while getting user media" &&
+          this.state.video.enabled) {
+        this.state.video.enabled = false;
+
+        window.MediaStreamTrack.getSources = function(callback) {
+          callback([{kind: "audio"}]);
+        };
+
+        this.stopListening(this.publisher);
+        this.publisher.destroy();
+        this.startPublishing();
+      }
+    },
+
+    /**
      * Publishes remote streams available once a session is connected.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
      *
      * @param  {SessionConnectEvent} event
      */
     startPublishing: function(event) {
       var outgoing = this.getDOMNode().querySelector(".local");
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -142,17 +142,18 @@ loop.shared.views = (function(_, OT, l10
    */
   var ConversationView = React.createClass({
     mixins: [Backbone.Events, sharedMixins.AudioMixin],
 
     propTypes: {
       sdk: React.PropTypes.object.isRequired,
       video: React.PropTypes.object,
       audio: React.PropTypes.object,
-      initiate: React.PropTypes.bool
+      initiate: React.PropTypes.bool,
+      isDesktop: React.PropTypes.bool
     },
 
     // height set to 100%" to fix video layout on Google Chrome
     // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
     publisherConfig: {
       insertMode: "append",
       width: "100%",
       height: "100%",
@@ -162,16 +163,17 @@ loop.shared.views = (function(_, OT, l10
         nameDisplayMode: "off",
         videoDisabledDisplayMode: "off"
       }
     },
 
     getDefaultProps: function() {
       return {
         initiate: true,
+        isDesktop: false,
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
       return {
         video: this.props.video,
@@ -182,16 +184,33 @@ loop.shared.views = (function(_, OT, l10
     componentWillMount: function() {
       if (this.props.initiate) {
         this.publisherConfig.publishVideo = this.props.video.enabled;
       }
     },
 
     componentDidMount: function() {
       if (this.props.initiate) {
+        /**
+         * XXX This is a workaround for desktop machines that do not have a
+         * camera installed. As we don't yet have device enumeration, when
+         * we do, this can be removed (bug 1138851), and the sdk should handle it.
+         */
+        if (this.props.isDesktop &&
+            !window.MediaStreamTrack.getSources) {
+          // If there's no getSources function, the sdk defines its own and caches
+          // the result. So here we define the "normal" one which doesn't get cached, so
+          // we can change it later.
+          window.MediaStreamTrack.getSources = function(callback) {
+            callback([{kind: "audio"}, {kind: "video"}]);
+          };
+        }
+
+        this.listenTo(this.props.sdk, "exception", this._handleSdkException.bind(this));
+
         this.listenTo(this.props.model, "session:connected",
                                         this._onSessionConnected);
         this.listenTo(this.props.model, "session:stream-created",
                                         this._streamCreated);
         this.listenTo(this.props.model, ["session:peer-hungup",
                                          "session:network-disconnected",
                                          "session:ended"].join(" "),
                                          this.stopPublishing);
@@ -246,16 +265,45 @@ loop.shared.views = (function(_, OT, l10
      * @param  {StreamEvent} event
      */
     _streamCreated: function(event) {
       var incoming = this.getDOMNode().querySelector(".remote");
       this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
     },
 
     /**
+     * Handles the SDK Exception event.
+     *
+     * https://tokbox.com/opentok/libraries/client/js/reference/ExceptionEvent.html
+     *
+     * @param {ExceptionEvent} event
+     */
+    _handleSdkException: function(event) {
+      /**
+       * XXX This is a workaround for desktop machines that do not have a
+       * camera installed. As we don't yet have device enumeration, when
+       * we do, this can be removed (bug 1138851), and the sdk should handle it.
+       */
+      if (this.publisher &&
+          event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
+          event.message === "Unknown Error while getting user media" &&
+          this.state.video.enabled) {
+        this.state.video.enabled = false;
+
+        window.MediaStreamTrack.getSources = function(callback) {
+          callback([{kind: "audio"}]);
+        };
+
+        this.stopListening(this.publisher);
+        this.publisher.destroy();
+        this.startPublishing();
+      }
+    },
+
+    /**
      * Publishes remote streams available once a session is connected.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
      *
      * @param  {SessionConnectEvent} event
      */
     startPublishing: function(event) {
       var outgoing = this.getDOMNode().querySelector(".local");
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -228,17 +228,18 @@ describe("loop.shared.views", function()
         subscribe: sandbox.spy()
       }, Backbone.Events);
       fakePublisher = _.extend({
         publishAudio: sandbox.spy(),
         publishVideo: sandbox.spy()
       }, Backbone.Events);
       fakeSDK = {
         initPublisher: sandbox.stub().returns(fakePublisher),
-        initSession: sandbox.stub().returns(fakeSession)
+        initSession: sandbox.stub().returns(fakeSession),
+        on: sandbox.stub()
       };
       model = new sharedModels.ConversationModel(fakeSessionData, {
         sdk: fakeSDK
       });
     });
 
     describe("#componentDidMount", function() {
       it("should start a session by default", function() {
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -120,17 +120,19 @@ describe("loop.webapp", function() {
         sdk: {}
       });
       conversation.set("loopToken", "fakeToken");
       ocView = mountTestComponent({
         helper: new sharedUtils.Helper(),
         client: client,
         conversation: conversation,
         notifications: notifications,
-        sdk: {},
+        sdk: {
+          on: sandbox.stub()
+        },
         feedbackStore: feedbackStore
       });
     });
 
     describe("start", function() {
       it("should display the StartConversationView", function() {
         TestUtils.findRenderedComponentWithType(ocView,
           loop.webapp.StartConversationView);