diff --git a/res/css/_components.scss b/res/css/_components.scss
index 37d0e0d286..0b46df9bd8 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -229,4 +229,4 @@
 @import "./views/verification/_VerificationShowSas.scss";
 @import "./views/voip/_CallContainer.scss";
 @import "./views/voip/_CallView.scss";
-@import "./views/voip/_VideoView.scss";
+@import "./views/voip/_VideoFeed.scss";
diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index 759797ae7b..eec8a1f188 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -33,11 +33,11 @@ limitations under the License.
         pointer-events: initial; // restore pointer events so the user can leave/interact
         cursor: pointer;
 
-        .mx_VideoView {
+        .mx_CallView_video {
             width: 350px;
         }
 
-        .mx_VideoView_localVideoFeed {
+        .mx_VideoFeed_local {
             border-radius: 8px;
             overflow: hidden;
         }
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index f6f3d40308..2aeaaa87dc 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -92,3 +92,10 @@ limitations under the License.
         background-color: $primary-fg-color;
     }
 }
+
+.mx_CallView_video {
+    width: 100%;
+    position: relative;
+    z-index: 30;
+}
+
diff --git a/res/css/views/voip/_VideoView.scss b/res/css/views/voip/_VideoFeed.scss
similarity index 72%
rename from res/css/views/voip/_VideoView.scss
rename to res/css/views/voip/_VideoFeed.scss
index feb60f4763..e5e3587dac 100644
--- a/res/css/views/voip/_VideoView.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015, 2016, 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,23 +14,17 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_VideoView {
-    width: 100%;
-    position: relative;
-    z-index: 30;
-}
-
-.mx_VideoView video {
+.mx_VideoFeed video {
     width: 100%;
 }
 
-.mx_VideoView_remoteVideoFeed {
+.mx_VideoFeed_remote {
     width: 100%;
     background-color: #000;
     z-index: 50;
 }
 
-.mx_VideoView_localVideoFeed {
+.mx_VideoFeed_local {
     width: 25%;
     height: 25%;
     position: absolute;
@@ -39,11 +33,11 @@ limitations under the License.
     z-index: 100;
 }
 
-.mx_VideoView_localVideoFeed video {
+.mx_VideoFeed_local video {
     width: auto;
     height: 100%;
 }
 
-.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video {
+.mx_VideoFeed_mirror video {
     transform: scale(-1, 1);
 }
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 0285107660..741798761f 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -69,6 +69,13 @@ declare global {
     interface Document {
         // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
         hasStorageAccess?: () => Promise<boolean>;
+
+        // Safari & IE11 only have this prefixed: we used prefixed versions
+        // previously so let's continue to support them for now
+        webkitExitFullscreen(): Promise<void>;
+        msExitFullscreen(): Promise<void>;
+        readonly webkitFullscreenElement: Element | null;
+        readonly msFullscreenElement: Element | null;
     }
 
     interface Navigator {
@@ -99,6 +106,13 @@ declare global {
         type?: string;
     }
 
+    interface Element {
+        // Safari & IE11 only have this prefixed: we used prefixed versions
+        // previously so let's continue to support them for now
+        webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
+        msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
+    }
+
     interface Error {
         // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName
         fileName?: string;
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index f3ce4ac679..710eded2cd 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -59,8 +59,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
 import PlatformPeg from './PlatformPeg';
 import Modal from './Modal';
 import { _t } from './languageHandler';
-// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
-import Matrix from 'matrix-js-sdk';
+import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import dis from './dispatcher/dispatcher';
 import WidgetUtils from './utils/WidgetUtils';
 import WidgetEchoStore from './stores/WidgetEchoStore';
@@ -77,7 +76,7 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
 import WidgetStore from "./stores/WidgetStore";
 import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
 import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
-import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/lib/webrtc/call";
+import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
 import Analytics from './Analytics';
 import CountlyAnalytics from "./CountlyAnalytics";
 
@@ -98,6 +97,21 @@ export enum PlaceCallType {
     ScreenSharing = 'screensharing',
 }
 
+function getRemoteAudioElement(): HTMLAudioElement {
+    // this needs to be somewhere at the top of the DOM which
+    // always exists to avoid audio interruptions.
+    // Might as well just use DOM.
+    const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
+    if (!remoteAudioElement) {
+        console.error(
+            "Failed to find remoteAudio element - cannot play audio!" +
+            "You need to add an <audio/> to the DOM.",
+        );
+        return null;
+    }
+    return remoteAudioElement;
+}
+
 export default class CallHandler {
     private calls = new Map<string, MatrixCall>();
     private audioPromises = new Map<AudioID, Promise<void>>();
@@ -291,6 +305,11 @@ export default class CallHandler {
         });
     }
 
+    private setCallAudioElement(call: MatrixCall) {
+        const audioElement = getRemoteAudioElement();
+        if (audioElement) call.setRemoteAudioElement(audioElement);
+    }
+
     private setCallState(call: MatrixCall, status: CallState) {
         console.log(
             `Call state in ${call.roomId} changed to ${status}`,
@@ -343,9 +362,11 @@ export default class CallHandler {
     ) {
         Analytics.trackEvent('voip', 'placeCall', 'type', type);
         CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
-        const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId);
+        const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
         this.calls.set(roomId, call);
         this.setCallListeners(call);
+        this.setCallAudioElement(call);
+
         if (type === PlaceCallType.Voice) {
             call.placeVoiceCall();
         } else if (type === 'video') {
@@ -451,6 +472,7 @@ export default class CallHandler {
                     Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
                     this.calls.set(call.roomId, call)
                     this.setCallListeners(call);
+                    this.setCallAudioElement(call);
                 }
                 break;
             case 'hangup':
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 5a44e4058b..9af5ebcbfb 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
 import SdkConfig from "./SdkConfig";
 import SettingsStore from "./settings/SettingsStore";
 import {UIFeature} from "./settings/UIFeature";
+import CallHandler from "./CallHandler";
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
 interface HTMLInputEvent extends Event {
@@ -1057,6 +1058,32 @@ export const Commands = [
         },
         category: CommandCategories.actions,
     }),
+    new Command({
+        command: "holdcall",
+        description: _td("Places the call in the current room on hold"),
+        category: CommandCategories.other,
+        runFn: function(roomId, args) {
+            const call = CallHandler.sharedInstance().getCallForRoom(roomId);
+            if (!call) {
+                return reject("No active call in this room");
+            }
+            call.setRemoteOnHold(true);
+            return success();
+        },
+    }),
+    new Command({
+        command: "unholdcall",
+        description: _td("Takes the call in the current room off hold"),
+        category: CommandCategories.other,
+        runFn: function(roomId, args) {
+            const call = CallHandler.sharedInstance().getCallForRoom(roomId);
+            if (!call) {
+                return reject("No active call in this room");
+            }
+            call.setRemoteOnHold(false);
+            return success();
+        },
+    }),
 
     // Command definitions for autocompletion ONLY:
     // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 0cb4a5d305..1c2bf3a000 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -71,7 +71,7 @@ import RoomHeader from "../views/rooms/RoomHeader";
 import TintableSvg from "../views/elements/TintableSvg";
 import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
-import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
+import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
 import WidgetStore from "../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../stores/AsyncStore";
 import Notifier from "../../Notifier";
diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx
index ca2b510f20..3d9235792b 100644
--- a/src/components/views/voip/CallPreview.tsx
+++ b/src/components/views/voip/CallPreview.tsx
@@ -24,7 +24,7 @@ import dis from '../../../dispatcher/dispatcher';
 import { ActionPayload } from '../../../dispatcher/payloads';
 import PersistentApp from "../elements/PersistentApp";
 import SettingsStore from "../../../settings/SettingsStore";
-import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
+import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 
 interface IProps {
 }
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 3e1833a903..653a72cca0 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -15,17 +15,18 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {createRef} from 'react';
+import React, { createRef } from 'react';
 import Room from 'matrix-js-sdk/src/models/room';
 import dis from '../../../dispatcher/dispatcher';
 import CallHandler from '../../../CallHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { _t } from '../../../languageHandler';
 import AccessibleButton from '../elements/AccessibleButton';
-import VideoView from "./VideoView";
+import VideoFeed, { VideoFeedType } from "./VideoFeed";
 import RoomAvatar from "../avatars/RoomAvatar";
 import PulsedAvatar from '../avatars/PulsedAvatar';
-import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
+import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
+import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
 
 interface IProps {
         // js-sdk room object. If set, we will only show calls for the given
@@ -50,53 +51,104 @@ interface IProps {
 }
 
 interface IState {
-    call: any;
+    call: MatrixCall;
+    isLocalOnHold: boolean,
+}
+
+function getFullScreenElement() {
+    return (
+        document.fullscreenElement ||
+        // moz omitted because firefox supports this unprefixed now (webkit here for safari)
+        document.webkitFullscreenElement ||
+        document.msFullscreenElement
+    );
+}
+
+function requestFullscreen(element: Element) {
+    const method = (
+        element.requestFullscreen ||
+        // moz omitted since firefox supports unprefixed now
+        element.webkitRequestFullScreen ||
+        element.msRequestFullscreen
+    );
+    if (method) method.call(element);
+}
+
+function exitFullscreen() {
+    const exitMethod = (
+        document.exitFullscreen ||
+        document.webkitExitFullscreen ||
+        document.msExitFullscreen
+    );
+    if (exitMethod) exitMethod.call(document);
 }
 
 export default class CallView extends React.Component<IProps, IState> {
-    private videoref: React.RefObject<any>;
     private dispatcherRef: string;
-    public call: any;
+    private container = createRef<HTMLDivElement>();
 
     constructor(props: IProps) {
         super(props);
 
+        const call = this.getCall();
         this.state = {
-            // the call this view is displaying (if any)
-            call: null,
-        };
+            call,
+            isLocalOnHold: call ? call.isLocalOnHold() : null,
+        }
 
-        this.videoref = createRef();
+        this.updateCallListeners(null, call);
     }
 
     public componentDidMount() {
         this.dispatcherRef = dis.register(this.onAction);
-        this.showCall();
     }
 
     public componentWillUnmount() {
+        this.updateCallListeners(this.state.call, null);
         dis.unregister(this.dispatcherRef);
     }
 
     private onAction = (payload) => {
-        // don't filter out payloads for room IDs other than props.room because
-        // we may be interested in the conf 1:1 room
-        if (payload.action !== 'call_state') {
-            return;
+        switch (payload.action) {
+            case 'video_fullscreen': {
+                if (!this.container.current) {
+                    return;
+                }
+                if (payload.fullscreen) {
+                    requestFullscreen(this.container.current);
+                } else if (getFullScreenElement()) {
+                    exitFullscreen();
+                }
+                break;
+            }
+            case 'call_state': {
+                const newCall = this.getCall();
+                if (newCall !== this.state.call) {
+                    this.updateCallListeners(this.state.call, newCall);
+                    this.setState({
+                        call: newCall,
+                        isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
+                    });
+                }
+                if (!newCall && getFullScreenElement()) {
+                    exitFullscreen();
+                }
+                break;
+            }
         }
-        this.showCall();
     };
 
-    private showCall() {
+    private getCall(): MatrixCall {
         let call: MatrixCall;
 
         if (this.props.room) {
             const roomId = this.props.room.roomId;
             call = CallHandler.sharedInstance().getCallForRoom(roomId);
 
-            if (this.call) {
-                this.setState({ call: call });
-            }
+            // We don't currently show voice calls in this view when in the room:
+            // they're represented in the room status bar at the bottom instead
+            // (but this will all change with the new designs)
+            if (call && call.type == CallType.Voice) call = null;
         } else {
             call = CallHandler.sharedInstance().getAnyActiveCall();
             // Ignore calls if we can't get the room associated with them.
@@ -106,65 +158,68 @@ export default class CallView extends React.Component<IProps, IState> {
             if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
                 call = null;
             }
-            this.setState({ call: call });
         }
 
-        if (call) {
-            if (this.getVideoView()) {
-                call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
-                call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
-
-                // always use a separate element for audio stream playback.
-                // this is to let us move CallView around the DOM without interrupting remote audio
-                // during playback, by having the audio rendered by a top-level <audio/> element.
-                // rather than being rendered by the main remoteVideo <video/> element.
-                call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
-            }
-        }
-        if (call && call.type === "video" && call.state !== CallState.Ended && call.state !== CallState.Ringing) {
-            this.getVideoView().getLocalVideoElement().style.display = "block";
-            this.getVideoView().getRemoteVideoElement().style.display = "block";
-        } else {
-            this.getVideoView().getLocalVideoElement().style.display = "none";
-            this.getVideoView().getRemoteVideoElement().style.display = "none";
-            dis.dispatch({action: 'video_fullscreen', fullscreen: false});
-        }
-
-        if (this.props.onResize) {
-            this.props.onResize();
-        }
+        if (call && call.state == CallState.Ended) return null;
+        return call;
     }
 
-    private getVideoView() {
-        return this.videoref.current;
+    private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
+        if (oldCall === newCall) return;
+
+        if (oldCall) oldCall.removeListener(CallEvent.HoldUnhold, this.onCallHoldUnhold);
+        if (newCall) newCall.on(CallEvent.HoldUnhold, this.onCallHoldUnhold);
     }
 
+    private onCallHoldUnhold = () => {
+        this.setState({
+            isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
+        });
+    };
+
     public render() {
         let view: React.ReactNode;
-        if (this.state.call && this.state.call.type === "voice") {
-            const client = MatrixClientPeg.get();
-            const callRoom = client.getRoom(this.state.call.roomId);
 
-            view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
-                <PulsedAvatar>
-                    <RoomAvatar
-                        room={callRoom}
-                        height={35}
-                        width={35}
+        if (this.state.call) {
+            if (this.state.call.type === "voice") {
+                const client = MatrixClientPeg.get();
+                const callRoom = client.getRoom(this.state.call.roomId);
+
+                let caption = _t("Active call");
+                if (this.state.isLocalOnHold) {
+                    // we currently have no UI for holding / unholding a call (apart from slash
+                    // commands) so we don't disintguish between when we've put the call on hold
+                    // (ie. we'd show an unhold button) and when the other side has put us on hold
+                    // (where obviously we would not show such a button).
+                    caption = _t("Call Paused");
+                }
+
+                view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
+                    <PulsedAvatar>
+                        <RoomAvatar
+                            room={callRoom}
+                            height={35}
+                            width={35}
+                        />
+                    </PulsedAvatar>
+                    <div>
+                        <h1>{callRoom.name}</h1>
+                        <p>{ caption }</p>
+                    </div>
+                </AccessibleButton>;
+            } else {
+                // For video calls, we currently ignore the call hold state altogether
+                // (the video will just go black)
+
+                // if we're fullscreen, we don't want to set a maxHeight on the video element.
+                const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight;
+                view = <div className="mx_CallView_video" onClick={this.props.onClick}>
+                    <VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
+                        maxHeight={maxVideoHeight}
                     />
-                </PulsedAvatar>
-                <div>
-                    <h1>{callRoom.name}</h1>
-                    <p>{ _t("Active call") }</p>
-                </div>
-            </AccessibleButton>;
-        } else {
-            view = <VideoView
-                ref={this.videoref}
-                onClick={this.props.onClick}
-                onResize={this.props.onResize}
-                maxHeight={this.props.maxVideoHeight}
-            />;
+                    <VideoFeed type={VideoFeedType.Local} call={this.state.call} />
+                </div>;
+            }
         }
 
         let hangup: React.ReactNode;
@@ -180,10 +235,9 @@ export default class CallView extends React.Component<IProps, IState> {
             />;
         }
 
-        return <div className={this.props.className}>
+        return <div className={this.props.className} ref={this.container}>
             {view}
             {hangup}
         </div>;
     }
 }
-
diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js
deleted file mode 100644
index a0330f8cb1..0000000000
--- a/src/components/views/voip/VideoFeed.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React, {createRef} from 'react';
-import PropTypes from 'prop-types';
-
-export default class VideoFeed extends React.Component {
-    static propTypes = {
-        // maxHeight style attribute for the video element
-        maxHeight: PropTypes.number,
-
-        // a callback which is called when the video element is resized
-        // due to a change in video metadata
-        onResize: PropTypes.func,
-    };
-
-    constructor(props) {
-        super(props);
-
-        this._vid = createRef();
-    }
-
-    componentDidMount() {
-        this._vid.current.addEventListener('resize', this.onResize);
-    }
-
-    componentWillUnmount() {
-        this._vid.current.removeEventListener('resize', this.onResize);
-    }
-
-    onResize = (e) => {
-        if (this.props.onResize) {
-            this.props.onResize(e);
-        }
-    };
-
-    render() {
-        return (
-            <video ref={this._vid} style={{maxHeight: this.props.maxHeight}}>
-            </video>
-        );
-    }
-}
-
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
new file mode 100644
index 0000000000..9dba9fa9c8
--- /dev/null
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -0,0 +1,80 @@
+/*
+Copyright 2015, 2016, 2019 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import classnames from 'classnames';
+import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
+import React, {createRef} from 'react';
+import SettingsStore from "../../../settings/SettingsStore";
+
+export enum VideoFeedType {
+    Local,
+    Remote,
+}
+
+interface IProps {
+    call: MatrixCall,
+
+    type: VideoFeedType,
+
+    // maxHeight style attribute for the video element
+    maxHeight?: number,
+
+    // a callback which is called when the video element is resized
+    // due to a change in video metadata
+    onResize?: (e: Event) => void,
+}
+
+export default class VideoFeed extends React.Component<IProps> {
+    private vid = createRef<HTMLVideoElement>();
+
+    componentDidMount() {
+        this.vid.current.addEventListener('resize', this.onResize);
+        if (this.props.type === VideoFeedType.Local) {
+            this.props.call.setLocalVideoElement(this.vid.current);
+        } else {
+            this.props.call.setRemoteVideoElement(this.vid.current);
+        }
+    }
+
+    componentWillUnmount() {
+        this.vid.current.removeEventListener('resize', this.onResize);
+    }
+
+    onResize = (e) => {
+        if (this.props.onResize) {
+            this.props.onResize(e);
+        }
+    };
+
+    render() {
+        const videoClasses = {
+            mx_VideoFeed: true,
+            mx_VideoFeed_local: this.props.type === VideoFeedType.Local,
+            mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote,
+            mx_VideoFeed_mirror: (
+                this.props.type === VideoFeedType.Local &&
+                SettingsStore.getValue('VideoView.flipVideoHorizontally')
+            ),
+        };
+
+        let videoStyle = {};
+        if (this.props.maxHeight) videoStyle = { maxHeight: this.props.maxHeight };
+
+        return <div className={classnames(videoClasses)}>
+            <video ref={this.vid} style={videoStyle}></video>
+        </div>;
+    }
+}
diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js
deleted file mode 100644
index 374a12e82d..0000000000
--- a/src/components/views/voip/VideoView.js
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React, {createRef} from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-
-import * as sdk from '../../../index';
-import dis from '../../../dispatcher/dispatcher';
-
-import SettingsStore from "../../../settings/SettingsStore";
-
-function getFullScreenElement() {
-    return (
-        document.fullscreenElement ||
-        document.mozFullScreenElement ||
-        document.webkitFullscreenElement ||
-        document.msFullscreenElement
-    );
-}
-
-export default class VideoView extends React.Component {
-    static propTypes = {
-        // maxHeight style attribute for the video element
-        maxHeight: PropTypes.number,
-
-        // a callback which is called when the user clicks on the video div
-        onClick: PropTypes.func,
-
-        // a callback which is called when the video element is resized due to
-        // a change in video metadata
-        onResize: PropTypes.func,
-    };
-
-    constructor(props) {
-        super(props);
-
-        this._local = createRef();
-        this._remote = createRef();
-    }
-
-    componentDidMount() {
-        this.dispatcherRef = dis.register(this.onAction);
-    }
-
-    componentWillUnmount() {
-        dis.unregister(this.dispatcherRef);
-    }
-
-    getRemoteVideoElement = () => {
-        return ReactDOM.findDOMNode(this._remote.current);
-    };
-
-    getRemoteAudioElement = () => {
-        // this needs to be somewhere at the top of the DOM which
-        // always exists to avoid audio interruptions.
-        // Might as well just use DOM.
-        const remoteAudioElement = document.getElementById("remoteAudio");
-        if (!remoteAudioElement) {
-            console.error("Failed to find remoteAudio element - cannot play audio!"
-                + "You need to add an <audio/> to the DOM.");
-        }
-        return remoteAudioElement;
-    };
-
-    getLocalVideoElement = () => {
-        return ReactDOM.findDOMNode(this._local.current);
-    };
-
-    setContainer = (c) => {
-        this.container = c;
-    };
-
-    onAction = (payload) => {
-        switch (payload.action) {
-            case 'video_fullscreen': {
-                if (!this.container) {
-                    return;
-                }
-                const element = this.container;
-                if (payload.fullscreen) {
-                    const requestMethod = (
-                        element.requestFullScreen ||
-                        element.webkitRequestFullScreen ||
-                        element.mozRequestFullScreen ||
-                        element.msRequestFullscreen
-                    );
-                    requestMethod.call(element);
-                } else if (getFullScreenElement()) {
-                    const exitMethod = (
-                        document.exitFullscreen ||
-                        document.mozCancelFullScreen ||
-                        document.webkitExitFullscreen ||
-                        document.msExitFullscreen
-                    );
-                    if (exitMethod) {
-                        exitMethod.call(document);
-                    }
-                }
-                break;
-            }
-        }
-    };
-
-    render() {
-        const VideoFeed = sdk.getComponent('voip.VideoFeed');
-
-        // if we're fullscreen, we don't want to set a maxHeight on the video element.
-        const maxVideoHeight = getFullScreenElement() ? null : this.props.maxHeight;
-        const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
-            { "mx_VideoView_localVideoFeed_flipped":
-                SettingsStore.getValue('VideoView.flipVideoHorizontally'),
-            },
-        );
-        return (
-            <div className="mx_VideoView" ref={this.setContainer} onClick={this.props.onClick}>
-                <div className="mx_VideoView_remoteVideoFeed">
-                    <VideoFeed ref={this._remote} onResize={this.props.onResize}
-                        maxHeight={maxVideoHeight} />
-                </div>
-                <div className={localVideoFeedClasses}>
-                    <VideoFeed ref={this._local} />
-                </div>
-            </div>
-        );
-    }
-}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 78340447f3..5fec27c7f6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -462,6 +462,8 @@
     "Send a bug report with logs": "Send a bug report with logs",
     "Opens chat with the given user": "Opens chat with the given user",
     "Sends a message to the given user": "Sends a message to the given user",
+    "Places the call in the current room on hold": "Places the call in the current room on hold",
+    "Takes the call in the current room off hold": "Takes the call in the current room off hold",
     "Displays action": "Displays action",
     "Reason": "Reason",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
@@ -777,6 +779,7 @@
     "My Ban List": "My Ban List",
     "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
     "Active call": "Active call",
+    "Call Paused": "Call Paused",
     "Unknown caller": "Unknown caller",
     "Incoming voice call": "Incoming voice call",
     "Incoming video call": "Incoming video call",