diff --git a/app/lib/components/PeerScreenView.jsx b/app/lib/components/PeerScreenView.jsx
new file mode 100644
index 0000000..62672aa
--- /dev/null
+++ b/app/lib/components/PeerScreenView.jsx
@@ -0,0 +1,160 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import Spinner from 'react-spinner';
+
+export default class PeerScreenView extends React.Component
+{
+ constructor(props)
+ {
+ super(props);
+
+ this.state =
+ {
+ videoWidth : null,
+ videoHeight : null
+ };
+
+ // Latest received video track.
+ // @type {MediaStreamTrack}
+ this._videoTrack = null;
+
+ // Periodic timer for showing video resolution.
+ this._videoResolutionTimer = null;
+ }
+
+ render()
+ {
+ const {
+ isMe,
+ videoVisible,
+ videoProfile,
+ videoCodec
+ } = this.props;
+
+ const {
+ videoWidth,
+ videoHeight
+ } = this.state;
+
+ return (
+
+
+
+
+ {videoCodec ?
+
{videoCodec} {videoProfile}
+ :null
+ }
+
+ {(videoVisible && videoWidth !== null) ?
+
{videoWidth}x{videoHeight}
+ :null
+ }
+
+
+
+
+
+
+ {videoProfile === 'none' ?
+
+
+
+ :null
+ }
+
+ );
+ }
+
+ componentDidMount()
+ {
+ const { videoTrack } = this.props;
+
+ this._setTracks(videoTrack);
+ }
+
+ componentWillUnmount()
+ {
+ clearInterval(this._videoResolutionTimer);
+ }
+
+ componentWillReceiveProps(nextProps)
+ {
+ const { videoTrack } = nextProps;
+
+ this._setTracks(videoTrack);
+ }
+
+ _setTracks(videoTrack)
+ {
+ if (this._videoTrack === videoTrack)
+ return;
+
+ this._videoTrack = videoTrack;
+
+ clearInterval(this._videoResolutionTimer);
+ this._hideVideoResolution();
+
+ const { video } = this.refs;
+
+ if (videoTrack)
+ {
+ const stream = new MediaStream;
+
+ if (videoTrack)
+ stream.addTrack(videoTrack);
+
+ video.srcObject = stream;
+
+ if (videoTrack)
+ this._showVideoResolution();
+ }
+ else
+ {
+ video.srcObject = null;
+ }
+ }
+
+ _showVideoResolution()
+ {
+ this._videoResolutionTimer = setInterval(() =>
+ {
+ const { videoWidth, videoHeight } = this.state;
+ const { video } = this.refs;
+
+ // Don't re-render if nothing changed.
+ if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
+ return;
+
+ this.setState(
+ {
+ videoWidth : video.videoWidth,
+ videoHeight : video.videoHeight
+ });
+ }, 1000);
+ }
+
+ _hideVideoResolution()
+ {
+ this.setState({ videoWidth: null, videoHeight: null });
+ }
+}
+
+PeerScreenView.propTypes =
+{
+ isMe : PropTypes.bool,
+ videoTrack : PropTypes.any,
+ videoVisible : PropTypes.bool.isRequired,
+ videoProfile : PropTypes.string,
+ videoCodec : PropTypes.string
+};
diff --git a/app/stylus/components/PeerScreenView.styl b/app/stylus/components/PeerScreenView.styl
new file mode 100644
index 0000000..d83e108
--- /dev/null
+++ b/app/stylus/components/PeerScreenView.styl
@@ -0,0 +1,146 @@
+[data-component='PeerScreenView'] {
+ position: relative;
+ flex: 100 100 auto;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background-color: rgba(#2a4b58, 0.9);
+ background-image: url('/resources/images/buddy.svg');
+ background-position: bottom;
+ background-size: auto 85%;
+ background-repeat: no-repeat;
+
+ > .info {
+ $backgroundTint = #000;
+
+ position: absolute;
+ z-index: 5
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background: linear-gradient(to bottom,
+ rgba($backgroundTint, 0) 0%,
+ rgba($backgroundTint, 0) 60%,
+ rgba($backgroundTint, 0.1) 70%,
+ rgba($backgroundTint, 0.8) 100%);
+
+ > .media {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: row;
+
+ > .box {
+ margin: 4px;
+ padding: 2px 4px;
+ border-radius: 2px;
+ background-color: rgba(#000, 0.25);
+
+ > p {
+ user-select: none;
+ pointer-events: none;
+ margin-bottom: 2px;
+ color: rgba(#fff, 0.7);
+ font-size: 10px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+
+ > .peer {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+
+ +desktop() {
+ &.is-me {
+ padding: 10px;
+ align-items: flex-start;
+ }
+
+ &:not(.is-me) {
+ padding: 20px;
+ align-items: flex-start;
+ }
+ }
+
+ +mobile() {
+ &.is-me {
+ padding: 10px;
+ align-items: flex-start;
+ }
+
+ &:not(.is-me) {
+ padding: 10px;
+ align-items: flex-end;
+ }
+ }
+ }
+ }
+
+ > video {
+ flex: 100 100 auto;
+ height: 100%;
+ width: 100%;
+ object-fit: cover;
+ user-select: none;
+ transition-property: opacity;
+ transition-duration: .15s;
+ background-color: rgba(#000, 0.75);
+
+ &.is-me {
+ transform: scaleX(-1);
+ }
+
+ &.hidden {
+ opacity: 0;
+ transition-duration: 0s;
+ }
+
+ &.loading {
+ filter: blur(5px);
+ }
+ }
+
+ > .spinner-container {
+ position: absolute;
+ top: 0
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: rgba(#000, 0.75);
+
+ .react-spinner {
+ position: relative;
+ width: 48px;
+ height: 48px;
+ top: 50%;
+ left: 50%;
+
+ .react-spinner_bar {
+ position: absolute;
+ width: 20%;
+ height: 7.8%;
+ top: -3.9%;
+ left: -10%;
+ animation: PeerView-spinner 1.2s linear infinite;
+ border-radius: 5px;
+ background-color: rgba(#fff, 0.5);
+ }
+ }
+ }
+}
+
+@keyframes PeerScreenView-spinner {
+ 0% { opacity: 1; }
+ 100% { opacity: 0.15; }
+}