Bug 1104930 - Create a mixin for handling updating of the video container for various Loop conversation views. r=nperriault
authorMark Banner <standard8@mozilla.com>
Fri, 16 Jan 2015 18:34:30 +0000
changeset 224227 68fbd452c2d95cf47a4d48297a0a77a28853eaa7
parent 224226 65325797108c163c54404d2591028f8981e0c9ae
child 224228 37ca802819327f29e4b7e7d2822b47cdf235bd81
push id28121
push userkwierso@gmail.com
push dateSat, 17 Jan 2015 00:45:57 +0000
treeherdermozilla-central@be3ff0fea299 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault
bugs1104930
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1104930 - Create a mixin for handling updating of the video container for various Loop conversation views. r=nperriault
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/js/mixins.js
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/desktop-local/roomViews_test.js
browser/components/loop/test/shared/mixins_test.js
browser/components/loop/test/shared/views_test.js
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -830,99 +830,46 @@ loop.conversationViews = (function(mozL1
             )
           )
         )
       );
     }
   });
 
   var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
+    mixins: [
+      sharedMixins.MediaSetupMixin
+    ],
+
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       video: React.PropTypes.object,
       audio: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     componentDidMount: function() {
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin.
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-        publisherConfig: this._getPublisherConfig(),
+        publisherConfig: this.getDefaultPublisherConfig({
+          publishVideo: this.props.video.enabled
+        }),
         getLocalElementFunc: this._getElement.bind(this, ".local"),
         getRemoteElementFunc: this._getElement.bind(this, ".remote")
       }));
     },
 
-    componentWillUnmount: function() {
-      window.removeEventListener('orientationchange', this.updateVideoContainer);
-      window.removeEventListener('resize', this.updateVideoContainer);
-    },
-
-    /**
-     * Returns either the required DOMNode
-     *
-     * @param {String} className The name of the class to get the element for.
-     */
-    _getElement: function(className) {
-      return this.getDOMNode().querySelector(className);
-    },
-
-    /**
-     * Returns the required configuration for publishing video on the sdk.
-     */
-    _getPublisherConfig: function() {
-      // height set to 100%" to fix video layout on Google Chrome
-      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
-      return {
-        insertMode: "append",
-        width: "100%",
-        height: "100%",
-        publishVideo: this.props.video.enabled,
-        style: {
-          audioLevelDisplayMode: "off",
-          bugDisplayMode: "off",
-          buttonDisplayMode: "off",
-          nameDisplayMode: "off",
-          videoDisabledDisplayMode: "off"
-        }
-      };
-    },
-
-    /**
-     * Used to update the video container whenever the orientation or size of the
-     * display area changes.
-     */
-    updateVideoContainer: function() {
-      var localStreamParent = this._getElement('.local .OT_publisher');
-      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
-    },
-
     /**
      * Hangs up the call.
      */
     hangup: function() {
       this.props.dispatcher.dispatch(
         new sharedActions.HangupCall());
     },
 
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -830,99 +830,46 @@ loop.conversationViews = (function(mozL1
             </button>
           </div>
         </div>
       );
     }
   });
 
   var OngoingConversationView = React.createClass({
+    mixins: [
+      sharedMixins.MediaSetupMixin
+    ],
+
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       video: React.PropTypes.object,
       audio: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     componentDidMount: function() {
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin.
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-        publisherConfig: this._getPublisherConfig(),
+        publisherConfig: this.getDefaultPublisherConfig({
+          publishVideo: this.props.video.enabled
+        }),
         getLocalElementFunc: this._getElement.bind(this, ".local"),
         getRemoteElementFunc: this._getElement.bind(this, ".remote")
       }));
     },
 
-    componentWillUnmount: function() {
-      window.removeEventListener('orientationchange', this.updateVideoContainer);
-      window.removeEventListener('resize', this.updateVideoContainer);
-    },
-
-    /**
-     * Returns either the required DOMNode
-     *
-     * @param {String} className The name of the class to get the element for.
-     */
-    _getElement: function(className) {
-      return this.getDOMNode().querySelector(className);
-    },
-
-    /**
-     * Returns the required configuration for publishing video on the sdk.
-     */
-    _getPublisherConfig: function() {
-      // height set to 100%" to fix video layout on Google Chrome
-      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
-      return {
-        insertMode: "append",
-        width: "100%",
-        height: "100%",
-        publishVideo: this.props.video.enabled,
-        style: {
-          audioLevelDisplayMode: "off",
-          bugDisplayMode: "off",
-          buttonDisplayMode: "off",
-          nameDisplayMode: "off",
-          videoDisabledDisplayMode: "off"
-        }
-      };
-    },
-
-    /**
-     * Used to update the video container whenever the orientation or size of the
-     * display area changes.
-     */
-    updateVideoContainer: function() {
-      var localStreamParent = this._getElement('.local .OT_publisher');
-      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
-    },
-
     /**
      * Hangs up the call.
      */
     hangup: function() {
       this.props.dispatcher.dispatch(
         new sharedActions.HangupCall());
     },
 
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -159,16 +159,17 @@ loop.roomViews = (function(mozL10n) {
 
   /**
    * Desktop room conversation view.
    */
   var DesktopRoomConversationView = React.createClass({displayName: "DesktopRoomConversationView",
     mixins: [
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
+      sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       feedbackStore:
         React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
     },
@@ -178,83 +179,32 @@ loop.roomViews = (function(mozL10n) {
         return React.createElement(DesktopRoomInvitationView, {
           roomStore: this.props.roomStore, 
           dispatcher: this.props.dispatcher}
         );
       }
       return null;
     },
 
-    componentDidMount: function() {
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin.
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-    },
-
     componentWillUpdate: function(nextProps, nextState) {
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-          publisherConfig: this._getPublisherConfig(),
+          publisherConfig: this.getDefaultPublisherConfig({
+            publishVideo: !this.state.videoMuted
+          }),
           getLocalElementFunc: this._getElement.bind(this, ".local"),
           getRemoteElementFunc: this._getElement.bind(this, ".remote")
         }));
       }
     },
 
-    _getPublisherConfig: function() {
-      // height set to 100%" to fix video layout on Google Chrome
-      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
-      return {
-        insertMode: "append",
-        width: "100%",
-        height: "100%",
-        publishVideo: !this.state.videoMuted,
-        style: {
-          audioLevelDisplayMode: "off",
-          bugDisplayMode: "off",
-          buttonDisplayMode: "off",
-          nameDisplayMode: "off",
-          videoDisabledDisplayMode: "off"
-        }
-      };
-    },
-
-    /**
-     * Used to update the video container whenever the orientation or size of the
-     * display area changes.
-     */
-    updateVideoContainer: function() {
-      var localStreamParent = this._getElement('.local .OT_publisher');
-      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
-    },
-
-    /**
-     * Returns either the required DOMNode
-     *
-     * @param {String} className The name of the class to get the element for.
-     */
-    _getElement: function(className) {
-      return this.getDOMNode().querySelector(className);
-    },
-
     /**
      * User clicked on the "Leave" button.
      */
     leaveRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
     },
 
     /**
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -159,16 +159,17 @@ loop.roomViews = (function(mozL10n) {
 
   /**
    * Desktop room conversation view.
    */
   var DesktopRoomConversationView = React.createClass({
     mixins: [
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
+      sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       feedbackStore:
         React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
     },
@@ -178,83 +179,32 @@ loop.roomViews = (function(mozL10n) {
         return <DesktopRoomInvitationView
           roomStore={this.props.roomStore}
           dispatcher={this.props.dispatcher}
         />;
       }
       return null;
     },
 
-    componentDidMount: function() {
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin.
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-    },
-
     componentWillUpdate: function(nextProps, nextState) {
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-          publisherConfig: this._getPublisherConfig(),
+          publisherConfig: this.getDefaultPublisherConfig({
+            publishVideo: !this.state.videoMuted
+          }),
           getLocalElementFunc: this._getElement.bind(this, ".local"),
           getRemoteElementFunc: this._getElement.bind(this, ".remote")
         }));
       }
     },
 
-    _getPublisherConfig: function() {
-      // height set to 100%" to fix video layout on Google Chrome
-      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
-      return {
-        insertMode: "append",
-        width: "100%",
-        height: "100%",
-        publishVideo: !this.state.videoMuted,
-        style: {
-          audioLevelDisplayMode: "off",
-          bugDisplayMode: "off",
-          buttonDisplayMode: "off",
-          nameDisplayMode: "off",
-          videoDisabledDisplayMode: "off"
-        }
-      };
-    },
-
-    /**
-     * Used to update the video container whenever the orientation or size of the
-     * display area changes.
-     */
-    updateVideoContainer: function() {
-      var localStreamParent = this._getElement('.local .OT_publisher');
-      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
-    },
-
-    /**
-     * Returns either the required DOMNode
-     *
-     * @param {String} className The name of the class to get the element for.
-     */
-    _getElement: function(className) {
-      return this.getDOMNode().querySelector(className);
-    },
-
     /**
      * User clicked on the "Leave" button.
      */
     leaveRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
     },
 
     /**
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -148,16 +148,85 @@ loop.shared.mixins = (function() {
 
     componentWillUnmount: function() {
       rootObject.document.removeEventListener(
         "visibilitychange", this._onDocumentVisibilityChanged);
     }
   };
 
   /**
+   * Media setup mixin. Provides a common location for settings for the media
+   * elements and handling updates of the media containers.
+   */
+  var MediaSetupMixin = {
+    componentDidMount: function() {
+      rootObject.addEventListener('orientationchange', this.updateVideoContainer);
+      rootObject.addEventListener('resize', this.updateVideoContainer);
+    },
+
+    componentWillUnmount: function() {
+      rootObject.removeEventListener('orientationchange', this.updateVideoContainer);
+      rootObject.removeEventListener('resize', this.updateVideoContainer);
+    },
+
+    /**
+     * Used to update the video container whenever the orientation or size of the
+     * display area changes.
+     */
+    updateVideoContainer: function() {
+      var localStreamParent = this._getElement('.local .OT_publisher');
+      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
+      if (localStreamParent) {
+        localStreamParent.style.width = "100%";
+      }
+      if (remoteStreamParent) {
+        remoteStreamParent.style.height = "100%";
+      }
+    },
+
+    /**
+     * Returns the default configuration for publishing media on the sdk.
+     *
+     * @param {Object} options An options object containing:
+     * - publishVideo A boolean set to true to publish video when the stream is initiated.
+     */
+    getDefaultPublisherConfig: function(options) {
+      options = options || {};
+      if (!"publishVideo" in options) {
+        throw new Error("missing option publishVideo");
+      }
+
+      // height set to 100%" to fix video layout on Google Chrome
+      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
+      return {
+        insertMode: "append",
+        width: "100%",
+        height: "100%",
+        publishVideo: options.publishVideo,
+        style: {
+          audioLevelDisplayMode: "off",
+          bugDisplayMode: "off",
+          buttonDisplayMode: "off",
+          nameDisplayMode: "off",
+          videoDisabledDisplayMode: "off"
+        }
+      };
+    },
+
+    /**
+     * Returns either the required DOMNode
+     *
+     * @param {String} className The name of the class to get the element for.
+     */
+    _getElement: function(className) {
+      return this.getDOMNode().querySelector(className);
+    }
+  };
+
+  /**
    * Audio mixin. Allows playing a single audio file and ensuring it
    * is stopped when the component is unmounted.
    */
   var AudioMixin = {
     audio: null,
     _audioRequest: null,
 
     _isLoopDesktop: function() {
@@ -303,12 +372,13 @@ loop.shared.mixins = (function() {
   return {
     AudioMixin: AudioMixin,
     RoomsAudioMixin: RoomsAudioMixin,
     setRootObject: setRootObject,
     DropdownMenuMixin: DropdownMenuMixin,
     DocumentVisibilityMixin: DocumentVisibilityMixin,
     DocumentLocationMixin: DocumentLocationMixin,
     DocumentTitleMixin: DocumentTitleMixin,
+    MediaSetupMixin: MediaSetupMixin,
     UrlHashChangeMixin: UrlHashChangeMixin,
     WindowCloseMixin: WindowCloseMixin
   };
 })();
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -136,93 +136,56 @@ loop.shared.views = (function(_, OT, l10
       );
     }
   });
 
   /**
    * Conversation view.
    */
   var ConversationView = React.createClass({displayName: "ConversationView",
-    mixins: [Backbone.Events, sharedMixins.AudioMixin],
+    mixins: [
+      Backbone.Events,
+      sharedMixins.AudioMixin,
+      sharedMixins.MediaSetupMixin
+    ],
 
     propTypes: {
       sdk: React.PropTypes.object.isRequired,
       video: React.PropTypes.object,
       audio: React.PropTypes.object,
       initiate: 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%",
-      style: {
-        audioLevelDisplayMode: "off",
-        bugDisplayMode: "off",
-        buttonDisplayMode: "off",
-        nameDisplayMode: "off",
-        videoDisabledDisplayMode: "off"
-      }
-    },
-
     getDefaultProps: function() {
       return {
         initiate: true,
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
       return {
         video: this.props.video,
         audio: this.props.audio
       };
     },
 
-    componentWillMount: function() {
-      if (this.props.initiate) {
-        this.publisherConfig.publishVideo = this.props.video.enabled;
-      }
-    },
-
     componentDidMount: function() {
       if (this.props.initiate) {
         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);
         this.props.model.startSession();
       }
-
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin.
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-    },
-
-    updateVideoContainer: function() {
-      var localStreamParent = document.querySelector('.local .OT_publisher');
-      var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
     },
 
     componentWillUnmount: function() {
       // Unregister all local event listeners
       this.stopListening();
       this.hangup();
     },
 
@@ -243,32 +206,35 @@ loop.shared.views = (function(_, OT, l10
      *      element.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
      *
      * @param  {StreamEvent} event
      */
     _streamCreated: function(event) {
       var incoming = this.getDOMNode().querySelector(".remote");
-      this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
+      this.props.model.subscribe(event.stream, incoming,
+        this.getDefaultPublisherConfig({
+          publishVideo: this.props.video.enabled
+        }));
     },
 
     /**
      * 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");
 
       // XXX move this into its StreamingVideo component?
       this.publisher = this.props.sdk.initPublisher(
-        outgoing, this.publisherConfig);
+        outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
 
       // Suppress OT GuM custom dialog, see bug 1018875
       this.listenTo(this.publisher, "accessDialogOpened accessDenied",
                     function(event) {
                       event.preventDefault();
                     });
 
       this.listenTo(this.publisher, "streamCreated", function(event) {
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -136,93 +136,56 @@ loop.shared.views = (function(_, OT, l10
       );
     }
   });
 
   /**
    * Conversation view.
    */
   var ConversationView = React.createClass({
-    mixins: [Backbone.Events, sharedMixins.AudioMixin],
+    mixins: [
+      Backbone.Events,
+      sharedMixins.AudioMixin,
+      sharedMixins.MediaSetupMixin
+    ],
 
     propTypes: {
       sdk: React.PropTypes.object.isRequired,
       video: React.PropTypes.object,
       audio: React.PropTypes.object,
       initiate: 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%",
-      style: {
-        audioLevelDisplayMode: "off",
-        bugDisplayMode: "off",
-        buttonDisplayMode: "off",
-        nameDisplayMode: "off",
-        videoDisabledDisplayMode: "off"
-      }
-    },
-
     getDefaultProps: function() {
       return {
         initiate: true,
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
       return {
         video: this.props.video,
         audio: this.props.audio
       };
     },
 
-    componentWillMount: function() {
-      if (this.props.initiate) {
-        this.publisherConfig.publishVideo = this.props.video.enabled;
-      }
-    },
-
     componentDidMount: function() {
       if (this.props.initiate) {
         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);
         this.props.model.startSession();
       }
-
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin.
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-    },
-
-    updateVideoContainer: function() {
-      var localStreamParent = document.querySelector('.local .OT_publisher');
-      var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
     },
 
     componentWillUnmount: function() {
       // Unregister all local event listeners
       this.stopListening();
       this.hangup();
     },
 
@@ -243,32 +206,35 @@ loop.shared.views = (function(_, OT, l10
      *      element.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
      *
      * @param  {StreamEvent} event
      */
     _streamCreated: function(event) {
       var incoming = this.getDOMNode().querySelector(".remote");
-      this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
+      this.props.model.subscribe(event.stream, incoming,
+        this.getDefaultPublisherConfig({
+          publishVideo: this.props.video.enabled
+        }));
     },
 
     /**
      * 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");
 
       // XXX move this into its StreamingVideo component?
       this.publisher = this.props.sdk.initPublisher(
-        outgoing, this.publisherConfig);
+        outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
 
       // Suppress OT GuM custom dialog, see bug 1018875
       this.listenTo(this.publisher, "accessDialogOpened accessDenied",
                     function(event) {
                       event.preventDefault();
                     });
 
       this.listenTo(this.publisher, "streamCreated", function(event) {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -189,16 +189,17 @@ loop.standaloneRoomViews = (function(moz
         )
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
     mixins: [
       Backbone.Events,
+      sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
@@ -226,71 +227,17 @@ loop.standaloneRoomViews = (function(moz
      * to match the store.
      *
      * @private
      */
     _onActiveRoomStateChanged: function() {
       this.setState(this.props.activeRoomStore.getStoreState());
     },
 
-    /**
-     * Returns either the required DOMNode
-     *
-     * @param {String} className The name of the class to get the element for.
-     */
-    _getElement: function(className) {
-      return this.getDOMNode().querySelector(className);
-    },
-
-     /**
-     * Returns the required configuration for publishing video on the sdk.
-     */
-    _getPublisherConfig: function() {
-      // height set to 100%" to fix video layout on Google Chrome
-      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
-      return {
-        insertMode: "append",
-        width: "100%",
-        height: "100%",
-        publishVideo: true,
-        style: {
-          audioLevelDisplayMode: "off",
-          bugDisplayMode: "off",
-          buttonDisplayMode: "off",
-          nameDisplayMode: "off",
-          videoDisabledDisplayMode: "off"
-        }
-      };
-    },
-
-    /**
-     * Used to update the video container whenever the orientation or size of the
-     * display area changes.
-     */
-    updateVideoContainer: function() {
-      var localStreamParent = this._getElement('.local .OT_publisher');
-      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
-    },
-
     componentDidMount: function() {
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin, bug 1104930
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-
       // Adding a class to the document body element from here to ease styling it.
       document.body.classList.add("is-standalone-room");
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.activeRoomStore);
     },
 
@@ -300,17 +247,17 @@ loop.standaloneRoomViews = (function(moz
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-          publisherConfig: this._getPublisherConfig(),
+          publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
           getLocalElementFunc: this._getElement.bind(this, ".local"),
           getRemoteElementFunc: this._getElement.bind(this, ".remote")
         }));
       }
 
       if (this.state.roomState !== ROOM_STATES.JOINED &&
           nextState.roomState === ROOM_STATES.JOINED) {
         // This forces the video size to update - creating the publisher
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -189,16 +189,17 @@ loop.standaloneRoomViews = (function(moz
         </footer>
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({
     mixins: [
       Backbone.Events,
+      sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
@@ -226,71 +227,17 @@ loop.standaloneRoomViews = (function(moz
      * to match the store.
      *
      * @private
      */
     _onActiveRoomStateChanged: function() {
       this.setState(this.props.activeRoomStore.getStoreState());
     },
 
-    /**
-     * Returns either the required DOMNode
-     *
-     * @param {String} className The name of the class to get the element for.
-     */
-    _getElement: function(className) {
-      return this.getDOMNode().querySelector(className);
-    },
-
-     /**
-     * Returns the required configuration for publishing video on the sdk.
-     */
-    _getPublisherConfig: function() {
-      // height set to 100%" to fix video layout on Google Chrome
-      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
-      return {
-        insertMode: "append",
-        width: "100%",
-        height: "100%",
-        publishVideo: true,
-        style: {
-          audioLevelDisplayMode: "off",
-          bugDisplayMode: "off",
-          buttonDisplayMode: "off",
-          nameDisplayMode: "off",
-          videoDisabledDisplayMode: "off"
-        }
-      };
-    },
-
-    /**
-     * Used to update the video container whenever the orientation or size of the
-     * display area changes.
-     */
-    updateVideoContainer: function() {
-      var localStreamParent = this._getElement('.local .OT_publisher');
-      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
-      }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
-    },
-
     componentDidMount: function() {
-      /**
-       * OT inserts inline styles into the markup. Using a listener for
-       * resize events helps us trigger a full width/height on the element
-       * so that they update to the correct dimensions.
-       * XXX: this should be factored as a mixin, bug 1104930
-       */
-      window.addEventListener('orientationchange', this.updateVideoContainer);
-      window.addEventListener('resize', this.updateVideoContainer);
-
       // Adding a class to the document body element from here to ease styling it.
       document.body.classList.add("is-standalone-room");
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.activeRoomStore);
     },
 
@@ -300,17 +247,17 @@ loop.standaloneRoomViews = (function(moz
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-          publisherConfig: this._getPublisherConfig(),
+          publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
           getLocalElementFunc: this._getElement.bind(this, ".local"),
           getRemoteElementFunc: this._getElement.bind(this, ".remote")
         }));
       }
 
       if (this.state.roomState !== ROOM_STATES.JOINED &&
           nextState.roomState === ROOM_STATES.JOINED) {
         // This forces the video size to update - creating the publisher
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -87,17 +87,19 @@ describe("loop.conversationViews", funct
           return"http://fakeurl";
         }
 
         return false;
     });
 
     fakeWindow = {
       navigator: { mozLoop: fakeMozLoop },
-      close: sandbox.stub(),
+      close: sinon.stub(),
+      addEventListener: function() {},
+      removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
   });
 
   afterEach(function() {
     loop.shared.mixins.setRootObject(window);
     document.title = oldTitle;
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -48,17 +48,19 @@ describe("loop.conversation", function()
       },
       getAudioBlob: sinon.spy(function(name, callback) {
         callback(null, new Blob([new ArrayBuffer(10)], {type: 'audio/ogg'}));
       })
     };
 
     fakeWindow = {
       navigator: { mozLoop: navigator.mozLoop },
-      close: sandbox.stub(),
+      close: sinon.stub(),
+      addEventListener: function() {},
+      removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
     // XXX These stubs should be hoisted in a common file
     // Bug 1040968
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -15,17 +15,19 @@ describe("loop.roomViews", function () {
     dispatcher = new loop.Dispatcher();
 
     fakeWindow = {
       document: {},
       navigator: {
         mozLoop: {
           getAudioBlob: sinon.stub()
         }
-      }
+      },
+      addEventListener: function() {},
+      removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
     // XXX These stubs should be hoisted in a common file
     // Bug 1040968
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -15,16 +15,17 @@ describe("loop.shared.mixins", function(
   var ROOM_STATES = loop.store.ROOM_STATES;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
   });
 
   afterEach(function() {
     sandbox.restore();
+    sharedMixins.setRootObject(window);
   });
 
   describe("loop.shared.mixins.UrlHashChangeMixin", function() {
     function createTestComponent(onUrlHashChange) {
       var TestComp = React.createClass({
         mixins: [loop.shared.mixins.UrlHashChangeMixin],
         onUrlHashChange: onUrlHashChange || function(){},
         render: function() {
@@ -157,20 +158,16 @@ describe("loop.shared.mixins", function(
         onDocumentHidden: onDocumentHiddenStub,
         onDocumentVisible: onDocumentVisibleStub,
         render: function() {
           return React.DOM.div();
         }
       });
     });
 
-    afterEach(function() {
-      loop.shared.mixins.setRootObject(window);
-    });
-
     function setupFakeVisibilityEventDispatcher(event) {
       loop.shared.mixins.setRootObject({
         document: {
           addEventListener: function(_, fn) {
             fn(event);
           },
           removeEventListener: sandbox.stub()
         }
@@ -191,16 +188,110 @@ describe("loop.shared.mixins", function(
         setupFakeVisibilityEventDispatcher({target: {hidden: true}});
 
         comp = TestUtils.renderIntoDocument(React.createElement(TestComp));
 
         sinon.assert.calledOnce(onDocumentHiddenStub);
       });
   });
 
+  describe("loop.shared.mixins.MediaSetupMixin", function() {
+    var view, TestComp, rootObject;
+
+    beforeEach(function() {
+      TestComp = React.createClass({
+        mixins: [loop.shared.mixins.MediaSetupMixin],
+        render: function() {
+          return React.DOM.div();
+        }
+      });
+
+      rootObject = {
+        events: {},
+        addEventListener: function(eventName, listener) {
+          this.events[eventName] = listener;
+        },
+        removeEventListener: function(eventName) {
+          delete this.events[eventName];
+        }
+      };
+
+      sharedMixins.setRootObject(rootObject);
+
+      view = TestUtils.renderIntoDocument(React.createElement(TestComp));
+    });
+
+    describe("#getDefaultPublisherConfig", function() {
+      it("should provide a default publisher configuration", function() {
+        var defaultConfig = view.getDefaultPublisherConfig({publishVideo: true});
+
+        expect(defaultConfig.publishVideo).eql(true);
+      });
+    });
+
+    describe("Events", function() {
+      var localElement, remoteElement;
+
+      beforeEach(function() {
+        sandbox.stub(view, "getDOMNode").returns({
+          querySelector: function(classSelector) {
+            if (classSelector.contains("local")) {
+              return localElement;
+            }
+            return remoteElement;
+          }
+        });
+      });
+
+      describe("resize", function() {
+        it("should update the width on the local stream element", function() {
+          localElement = {
+            style: { width: "0%" }
+          };
+
+          rootObject.events.resize();
+
+          expect(localElement.style.width).eql("100%");
+        });
+
+        it("should update the height on the remote stream element", function() {
+          remoteElement = {
+            style: { height: "0%" }
+          };
+
+          rootObject.events.resize();
+
+          expect(remoteElement.style.height).eql("100%");
+        });
+      });
+
+      describe("orientationchange", function() {
+        it("should update the width on the local stream element", function() {
+          localElement = {
+            style: { width: "0%" }
+          };
+
+          rootObject.events.orientationchange();
+
+          expect(localElement.style.width).eql("100%");
+        });
+
+        it("should update the height on the remote stream element", function() {
+          remoteElement = {
+            style: { height: "0%" }
+          };
+
+          rootObject.events.orientationchange();
+
+          expect(remoteElement.style.height).eql("100%");
+        });
+      });
+    });
+  });
+
   describe("loop.shared.mixins.AudioMixin", function() {
     var view, fakeAudio, TestComp;
 
     beforeEach(function() {
       navigator.mozLoop = {
         doNotDisturb: true,
         getAudioBlob: sinon.spy(function(name, callback) {
           callback(null, new Blob([new ArrayBuffer(10)], {type: 'audio/ogg'}));
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -260,28 +260,16 @@ describe("loop.shared.views", function()
           initiate: false,
           sdk: fakeSDK,
           model: model,
           video: {enabled: true}
         });
 
         sinon.assert.notCalled(model.startSession);
       });
-
-      it("should set the correct stream publish options", function() {
-
-        var component = mountTestComponent({
-          sdk: fakeSDK,
-          model: model,
-          video: {enabled: false}
-        });
-
-        expect(component.publisherConfig.publishVideo).to.eql(false);
-
-      });
     });
 
     describe("constructed", function() {
       var comp;
 
       beforeEach(function() {
         comp = mountTestComponent({
           sdk: fakeSDK,