Repository: microsoft/vscode-edge-devtools
Pull request #513 (2021-09-23 18:59:34)

Introducing standalone screencast
Introduces a new screencast experience that:

- Allowed the user to mirror the screencast contents in a standalone tab.
- Enabled mouse events for the screencast view
- Enable emulation for a mobile device.
- Added a setting to toggle on and off standalone screencast: Users can now enable the new experience or fallback to the previous behavior.
- Moved the screencast css and html to a template file
- Added a device list that mirrors the one in devtools, this device list will change the mouse to touch for mobile and touch devices and will stay as a mouse with click events for Desktop and Laptops.
- Integrated the screencast into the existing devtools: the new screencast version can be launched by using the screencast button on the devtools.
- Wire keyboard events so that users can type and use the keyboard in the screencast view.
- Added the option to launch screencast without launching devtools: before a devtools instance was required to launch the new screencast panel, but now a new button (on the target list) allows the user to launch (preview) the url using screencast.



Patch:
diff --git a/package-lock.json b/package-lock.json
index 6e3b8f1..321255c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1781,6 +1781,12 @@
                 "eslint-visitor-keys": "^2.0.0"
             }
         },
+        "@vscode/codicons": {
+            "version": "0.0.22",
+            "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.22.tgz",
+            "integrity": "sha512-Cl3s2vgRhoJJ5nY2qC3JXCF0uoSw4zaMi0ZJIIcyOQTeeQCXIirE522cIRlcIo8cJDspQY9PMgALGvD0afZ5Mg==",
+            "dev": true
+        },
         "@webassemblyjs/ast": {
             "version": "1.11.0",
             "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz",
diff --git a/package.json b/package.json
index 4e60781..b3ff375 100644
--- a/package.json
+++ b/package.json
@@ -102,6 +102,16 @@
                     "dark": "resources/dark/close_browser_page.svg"
                 }
             },
+            {
+                "command": "vscode-edge-devtools-view.toggleScreencast",
+                "category": "Microsoft Edge Tools",
+                "title": "Toggle screencast",
+                "enablement": "viewItem != cdpTargetClosing",
+                "icon": {
+                    "light": "resources/light/screencast.svg",
+                    "dark": "resources/dark/screencast.svg"
+                }
+            },
             {
                 "command": "vscode-edge-devtools-view.copyItem",
                 "category": "Microsoft Edge Tools",
@@ -196,6 +206,11 @@
                     },
                     "description": "A set of mappings for rewriting the locations of source files from what the sourcemap says, to their locations on disk"
                 },
+                "vscode-edge-devtools.standaloneScreencast": {
+                    "type": "boolean",
+                    "default": false,
+                    "description": "When selected, screencast will be rendered in an individual screen and enable device emulation."
+                },
                 "vscode-edge-devtools.sourceMapPathOverrides": {
                     "type": "object",
                     "description": "A set of mappings to override the locations of source map files.",
@@ -529,15 +544,20 @@
                 }
             ],
             "view/item/context": [
+                {
+                    "command": "vscode-edge-devtools-view.toggleScreencast",
+                    "when": "view == vscode-edge-devtools-view.targets && viewItem == cdpTarget && standaloneScreencast",
+                    "group": "inline@0"
+                },
                 {
                     "command": "vscode-edge-devtools-view.attach",
                     "when": "view == vscode-edge-devtools-view.targets && viewItem == cdpTarget || viewItem == cdpTargetClosing",
-                    "group": "inline"
+                    "group": "inline@1"
                 },
                 {
                     "command": "vscode-edge-devtools-view.close-instance",
                     "when": "view == vscode-edge-devtools-view.targets && viewItem == cdpTarget || viewItem == cdpTargetClosing",
-                    "group": "inline"
+                    "group": "inline@2"
                 },
                 {
                     "command": "vscode-edge-devtools-view.attach",
@@ -690,6 +710,7 @@
         "@types/ws": "7.4.2",
         "@typescript-eslint/eslint-plugin": "4.22.1",
         "@typescript-eslint/parser": "4.22.1",
+        "@vscode/codicons": "0.0.22",
         "copy-webpack-plugin": "8.1.1",
         "eslint": "7.25.0",
         "eslint-plugin-import": "2.22.1",
diff --git a/resources/dark/screencast.svg b/resources/dark/screencast.svg
new file mode 100644
index 0000000..629752a
--- /dev/null
+++ b/resources/dark/screencast.svg
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="22"
+   height="22"
+   version="1.1"
+   id="svg5288"
+   sodipodi:docname="screencast.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)">
+  <metadata
+     id="metadata5294">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs5292" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2560"
+     inkscape:window-height="1377"
+     id="namedview5290"
+     showgrid="true"
+     inkscape:zoom="23.627417"
+     inkscape:cx="8.4232539"
+     inkscape:cy="5.3953736"
+     inkscape:window-x="1192"
+     inkscape:window-y="1432"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg5288"
+     inkscape:snap-bbox="true">
+    <inkscape:grid
+       originx="0"
+       originy="0"
+       type="xygrid"
+       id="grid8172" />
+  </sodipodi:namedview>
+    <path
+      style="fill:#c5c5c5;fill-opacity:1;stroke:none;stroke-width:0.20000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+     d="M 5,2 C 4.4211794,2 3.9831529,2.5466154 3.9831529,3.1750769 v 1.748 H 2.7797834 v -1.748 C 2.7797834,1.6989231 3.7906138,0.5384615 5,0.5384615 h 12.406739 c 1.209386,0 2.220216,1.1604616 2.220216,2.6366154 V 13.978769 c 0,1.476154 -1.01083,2.636616 -2.220216,2.636616 H 10 v -1.461539 h 7.406739 c 0.578821,0 1.016847,-0.546615 1.016847,-1.175077 V 3.1750769 C 18.423586,2.5466154 17.98556,2 17.406739,2 Z m 5,11.692308 h 3.008424 c 0.33213,0 0.601684,-0.327385 0.601684,-0.730769 0,-0.403385 -0.269554,-0.73077 -0.601684,-0.73077 H 10 Z m -6.0168471,4.384615 c -0.33213,0 -0.6016847,0.327385 -0.6016847,0.730769 0,0.403385 0.2695547,0.730769 0.6016847,0.730769 h 1.2033694 c 0.33213,0 0.6016847,-0.327384 0.6016847,-0.730769 0,-0.403384 -0.2695547,-0.730769 -0.6016847,-0.730769 z m -3.6101083,-9.5 c 0,-1.2101538 0.8086642,-2.1923076 1.8050541,-2.1923076 h 4.8134777 c 0.9963899,0 1.8050546,0.9821538 1.8050546,2.1923076 V 20.26923 c 0,1.210154 -0.8086647,2.192308 -1.8050546,2.192308 H 2.1780987 c -0.9963899,0 -1.8050541,-0.982154 -1.8050541,-2.192308 z M 2.1780987,7.8461539 c -0.3321299,0 -0.6016847,0.3273841 -0.6016847,0.7307691 V 20.26923 C 1.576414,20.672616 1.8459688,21 2.1780987,21 h 4.8134777 c 0.33213,0 0.6016847,-0.327384 0.6016847,-0.73077 V 8.576923 c 0,-0.403385 -0.2695547,-0.7307691 -0.6016847,-0.7307691 z"
+     id="path198" />
+</svg>
diff --git a/resources/light/screencast.svg b/resources/light/screencast.svg
new file mode 100644
index 0000000..8a5eb15
--- /dev/null
+++ b/resources/light/screencast.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="22"
+   height="22"
+   version="1.1"
+   id="svg5288"
+   sodipodi:docname="screencast.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)">
+  <metadata
+     id="metadata5294">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs5292" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2560"
+     inkscape:window-height="1377"
+     id="namedview5290"
+     showgrid="true"
+     inkscape:zoom="23.627417"
+     inkscape:cx="8.4232539"
+     inkscape:cy="5.3953736"
+     inkscape:window-x="1192"
+     inkscape:window-y="1432"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg5288"
+     inkscape:snap-bbox="true">
+    <inkscape:grid
+       originx="0"
+       originy="0"
+       type="xygrid"
+       id="grid8172" />
+  </sodipodi:namedview>
+    <path
+      style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.20000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+     d="M 5,2 C 4.4211794,2 3.9831529,2.5466154 3.9831529,3.1750769 v 1.748 H 2.7797834 v -1.748 C 2.7797834,1.6989231 3.7906138,0.5384615 5,0.5384615 h 12.406739 c 1.209386,0 2.220216,1.1604616 2.220216,2.6366154 V 13.978769 c 0,1.476154 -1.01083,2.636616 -2.220216,2.636616 H 10 v -1.461539 h 7.406739 c 0.578821,0 1.016847,-0.546615 1.016847,-1.175077 V 3.1750769 C 18.423586,2.5466154 17.98556,2 17.406739,2 Z m 5,11.692308 h 3.008424 c 0.33213,0 0.601684,-0.327385 0.601684,-0.730769 0,-0.403385 -0.269554,-0.73077 -0.601684,-0.73077 H 10 Z m -6.0168471,4.384615 c -0.33213,0 -0.6016847,0.327385 -0.6016847,0.730769 0,0.403385 0.2695547,0.730769 0.6016847,0.730769 h 1.2033694 c 0.33213,0 0.6016847,-0.327384 0.6016847,-0.730769 0,-0.403384 -0.2695547,-0.730769 -0.6016847,-0.730769 z m -3.6101083,-9.5 c 0,-1.2101538 0.8086642,-2.1923076 1.8050541,-2.1923076 h 4.8134777 c 0.9963899,0 1.8050546,0.9821538 1.8050546,2.1923076 V 20.26923 c 0,1.210154 -0.8086647,2.192308 -1.8050546,2.192308 H 2.1780987 c -0.9963899,0 -1.8050541,-0.982154 -1.8050541,-2.192308 z M 2.1780987,7.8461539 c -0.3321299,0 -0.6016847,0.3273841 -0.6016847,0.7307691 V 20.26923 C 1.576414,20.672616 1.8459688,21 2.1780987,21 h 4.8134777 c 0.33213,0 0.6016847,-0.327384 0.6016847,-0.73077 V 8.576923 c 0,-0.403385 -0.2695547,-0.7307691 -0.6016847,-0.7307691 z"
+     id="path198" />
+</svg>
+
diff --git a/src/common/errorCodes.ts b/src/common/errorCodes.ts
new file mode 100644
index 0000000..427ec96
--- /dev/null
+++ b/src/common/errorCodes.ts
@@ -0,0 +1,5 @@
+export const enum ErrorCodes {
+  Error,
+  Warning,
+  Informational
+}
diff --git a/src/common/settingsProvider.ts b/src/common/settingsProvider.ts
index 1652e19..c24e2e8 100644
--- a/src/common/settingsProvider.ts
+++ b/src/common/settingsProvider.ts
@@ -62,6 +62,13 @@ export class SettingsProvider {
     return isHeadless;
   }
 
+  getScreencastSettings(): boolean {
+    const settings = vscode.workspace.getConfiguration(SETTINGS_STORE_NAME);
+    const screencastSetting = settings.get('standaloneScreencast');
+    const standaloneScreencast: boolean = screencastSetting !== undefined ? !!screencastSetting : false;
+    return standaloneScreencast;
+  }
+
   static get instance(): SettingsProvider {
     if (!SettingsProvider.singletonInstance) {
       SettingsProvider.singletonInstance = new SettingsProvider();
diff --git a/src/common/webviewEvents.ts b/src/common/webviewEvents.ts
index b4fddeb..0867fff 100644
--- a/src/common/webviewEvents.ts
+++ b/src/common/webviewEvents.ts
@@ -2,7 +2,7 @@
 // Licensed under the MIT License.
 
 export type WebviewEvent = 'getState' | 'getUrl' | 'openInEditor' | 'cssMirrorContent' | 'ready' | 'setState' | 'telemetry' | 'websocket'
-| 'getVscodeSettings' | 'copyText' | 'focusEditor' | 'focusEditorGroup' | 'openUrl';
+| 'getVscodeSettings' | 'copyText' | 'focusEditor' | 'focusEditorGroup' | 'openUrl' | 'toggleScreencast';
 export const webviewEventNames: WebviewEvent[] = [
     'getState',
     'getUrl',
@@ -17,10 +17,11 @@ export const webviewEventNames: WebviewEvent[] = [
     'focusEditor',
     'focusEditorGroup',
     'openUrl',
+    'toggleScreencast',
 ];
 
 export type FrameToolsEvent = 'sendMessageToBackend' | 'openInNewTab' | 'recordEnumeratedHistogram' |
-'recordPerformanceHistogram' | 'reportError' | 'openInEditor' | 'cssMirrorContent';
+'recordPerformanceHistogram' | 'reportError' | 'openInEditor' | 'cssMirrorContent' | 'toggleScreencast';
 export const FrameToolsEventNames: FrameToolsEvent[] = [
     'sendMessageToBackend',
     'openInNewTab',
@@ -29,6 +30,7 @@ export const FrameToolsEventNames: FrameToolsEvent[] = [
     'recordEnumeratedHistogram',
     'recordPerformanceHistogram',
     'reportError',
+    'toggleScreencast',
 ];
 
 export type WebSocketEvent = 'open' | 'close' | 'error' | 'message';
diff --git a/src/devtoolsPanel.ts b/src/devtoolsPanel.ts
index bd9e423..067247f 100644
--- a/src/devtoolsPanel.ts
+++ b/src/devtoolsPanel.ts
@@ -28,8 +28,10 @@ import {
     SETTINGS_PREF_NAME,
     SETTINGS_STORE_NAME,
     SETTINGS_WEBVIEW_NAME,
+    SETTINGS_VIEW_NAME,
 } from './utils';
-import { ErrorCodes, ErrorReporter } from './errorReporter';
+import { ErrorReporter } from './errorReporter';
+import { ErrorCodes } from './common/errorCodes';
 
 export class DevToolsPanel {
     private static instance: DevToolsPanel | undefined;
@@ -77,6 +79,7 @@ export class DevToolsPanel {
         this.panelSocket.on('getUrl', msg => this.onSocketGetUrl(msg) as unknown as void);
         this.panelSocket.on('openUrl', msg => this.onSocketOpenUrl(msg) as unknown as void);
         this.panelSocket.on('openInEditor', msg => this.onSocketOpenInEditor(msg) as unknown as void);
+        this.panelSocket.on('toggleScreencast', () => this.toggleScreencast() as unknown as void);
         this.panelSocket.on('cssMirrorContent', msg => this.onSocketCssMirrorContent(msg) as unknown as void);
         this.panelSocket.on('close', () => this.onSocketClose());
         this.panelSocket.on('copyText', msg => this.onSocketCopyText(msg));
@@ -192,6 +195,11 @@ export class DevToolsPanel {
         }
     }
 
+    private toggleScreencast() {
+        const websocketUrl = this.targetUrl;
+        void vscode.commands.executeCommand(`${SETTINGS_VIEW_NAME}.toggleScreencast`, { websocketUrl });
+    }
+
     private onSocketTelemetry(message: string) {
         const telemetry: TelemetryData = JSON.parse(message) as TelemetryData;
 
@@ -454,6 +462,7 @@ export class DevToolsPanel {
         const stylesUri = this.panel.webview.asWebviewUri(stylesPath);
 
         const theme = SettingsProvider.instance.getThemeFromUserSetting();
+        const standaloneScreencast = SettingsProvider.instance.getScreencastSettings();
 
         // the added fields for "Content-Security-Policy" allow resource loading for other file types
         return `
@@ -474,7 +483,7 @@ export class DevToolsPanel {
                 ">
             </head>
             <body>
-                <iframe id="devtools-frame" frameBorder="0" src="${cdnBaseUri}?experiments=true&theme=${theme}&headless=${this.isHeadless}"></iframe>
+                <iframe id="devtools-frame" frameBorder="0" src="${cdnBaseUri}?experiments=true&theme=${theme}&headless=${this.isHeadless}&standaloneScreencast=${standaloneScreencast}"></iframe>
                 <div id="error-message" class="hidden">
                     <h1>Unable to download DevTools for the current target.</h1>
                     <p>Try these troubleshooting steps:</p>
diff --git a/src/errorReporter.ts b/src/errorReporter.ts
index 5ee4d04..86c1fb9 100644
--- a/src/errorReporter.ts
+++ b/src/errorReporter.ts
@@ -1,11 +1,6 @@
 
 import * as vscode from 'vscode';
-
-export const enum ErrorCodes {
-  Error,
-  Warning,
-  Informational
-}
+import { ErrorCodes } from './common/errorCodes';
 
 export interface ErrorEventInterface {
   title: string,
diff --git a/src/extension.ts b/src/extension.ts
index 2db10af..626eb42 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -8,6 +8,7 @@ import TelemetryReporter from 'vscode-extension-telemetry';
 import { CDPTarget } from './cdpTarget';
 import { CDPTargetsProvider } from './cdpTargetsProvider';
 import { DevToolsPanel } from './devtoolsPanel';
+import { ScreencastPanel } from './screencastPanel';
 import { LaunchDebugProvider } from './launchDebugProvider';
 import {
     buttonCode,
@@ -33,7 +34,9 @@ import {
     reportUrlType,
 } from './utils';
 import { LaunchConfigManager } from './launchConfigManager';
-import { ErrorCodes, ErrorReporter } from './errorReporter';
+import { ErrorReporter } from './errorReporter';
+import { SettingsProvider } from './common/settingsProvider';
+import { ErrorCodes } from './common/errorCodes';
 
 let telemetryReporter: Readonly<TelemetryReporter>;
 let browserInstance: Browser;
@@ -45,6 +48,10 @@ export function activate(context: vscode.ExtensionContext): void {
         telemetryReporter = createTelemetryReporter(context);
     }
 
+    // enable/disable standalone screencast target panel icon.
+    const standaloneScreencast = SettingsProvider.instance.getScreencastSettings();
+    void vscode.commands.executeCommand('setContext', 'standaloneScreencast', standaloneScreencast);
+
     // Check if launch.json exists and has supported config to populate side pane welcome message
     LaunchConfigManager.instance.updateLaunchConfig();
     context.subscriptions.push(vscode.commands.registerCommand(`${SETTINGS_STORE_NAME}.attach`, (): void => {
@@ -103,6 +110,20 @@ export function activate(context: vscode.ExtensionContext): void {
             const runtimeConfig = getRuntimeConfig();
             DevToolsPanel.createOrShow(context, telemetryReporter, target.websocketUrl, runtimeConfig);
         }));
+
+    context.subscriptions.push(vscode.commands.registerCommand(
+        `${SETTINGS_VIEW_NAME}.toggleScreencast`,
+        (target?: CDPTarget) => {
+            if (!target){
+                const errorMessage = 'No target selected';
+                telemetryReporter.sendTelemetryErrorEvent('command/screencast/target', {message: errorMessage});
+                return;
+            }
+            telemetryReporter.sendTelemetryEvent('user/buttonPress', { 'VSCode.buttonCode': buttonCode.toggleScreencast });
+            telemetryReporter.sendTelemetryEvent('view/screencast');
+            ScreencastPanel.createOrShow(context, target.websocketUrl);
+        }));
+
     context.subscriptions.push(vscode.commands.registerCommand(`${SETTINGS_VIEW_NAME}.openSettings`, () => {
         telemetryReporter.sendTelemetryEvent('user/buttonPress', { 'VSCode.buttonCode': buttonCode.openSettings });
         void vscode.commands.executeCommand('workbench.action.openSettings', `${SETTINGS_STORE_NAME}`);
diff --git a/src/host_beta/messageRouter.ts b/src/host_beta/messageRouter.ts
index 9c515f1..e7b512a 100644
--- a/src/host_beta/messageRouter.ts
+++ b/src/host_beta/messageRouter.ts
@@ -94,6 +94,9 @@ export class MessageRouter {
                 const [cdpMessage] = args;
                 this.sendMessageToBackend(cdpMessage);
                 return true;
+            case 'toggleScreencast':
+                this.toggleScreencast()
+                return true;
             default:
                 // TODO: handle other types of messages from devtools
                 return false;
@@ -157,6 +160,11 @@ export class MessageRouter {
         encodeMessageForChannel(msg => vscode.postMessage(msg, '*'), 'openInEditor', request);
     }
 
+    private toggleScreencast(): void {
+        // Forward the data to the extension
+        encodeMessageForChannel(msg => vscode.postMessage(msg, '*'), 'toggleScreencast');
+    }
+
     private cssMirrorContent(url: string, newContent: string): void {
         // Forward the data to the extension
         const request: ICssMirrorContentData = { url, newContent };
diff --git a/src/screencast/cdp.ts b/src/screencast/cdp.ts
new file mode 100644
index 0000000..acbb642
--- /dev/null
+++ b/src/screencast/cdp.ts
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { encodeMessageForChannel, parseMessageFromChannel, WebSocketEvent } from "../common/webviewEvents";
+
+declare const acquireVsCodeApi: () => {postMessage(message: unknown, args?: any|undefined): void};
+export const vscode = acquireVsCodeApi();
+
+interface CdpMessage {
+    id: number;
+    method: string;
+    params?: any;
+    result?: any;
+}
+
+export type CdpEventCallback = (params: any) => void;
+export type CdpMethodCallback = (result: any) => void;
+
+export class ScreencastCDPConnection {
+    private nextId: number = 0;
+    private eventCallbackMap: Map<string, CdpEventCallback[]> = new Map();
+    private methodCallbackMap: Map<number, CdpMethodCallback> = new Map();
+
+    constructor() {
+        // Handle CDP messages/events routed from the extension through post message
+        window.addEventListener('message', e => {
+            parseMessageFromChannel(e.data, (eventName, args) => {
+                if (eventName === 'websocket') {
+                    const { event, message } = JSON.parse(args) as {event: WebSocketEvent, message: string};
+                    console.log(event, message);
+                    // Handle event responses
+                    const messageObj = JSON.parse(message) as CdpMessage;
+                    for (const callback of this.eventCallbackMap.get(messageObj.method) || []) {
+                        callback(messageObj.params)
+                    }
+                    // Handle method responses
+                    const methodCallback = this.methodCallbackMap.get(messageObj.id);
+                    if (methodCallback) {
+                        methodCallback(messageObj.result);
+                        this.methodCallbackMap.delete(messageObj.id);
+                    }
+                    return true;
+                }
+                return false;
+            });
+        });
+    }
+
+    sendMessageToBackend(method: string, params: any, callback?: CdpMethodCallback): void {
+        const id = this.nextId++
+        const cdpMessage: CdpMessage = {
+            id: id,
+            method,
+            params
+        }
+        if (callback) {
+            this.methodCallbackMap.set(id, callback);
+        }
+        encodeMessageForChannel(msg => vscode.postMessage(msg, '*'), 'websocket', { message: JSON.stringify(cdpMessage) });
+    }
+
+    registerForEvent(method: string, callback: CdpEventCallback): void {
+        if (this.eventCallbackMap.has(method)) {
+            this.eventCallbackMap.get(method)?.push(callback);
+        }
+        this.eventCallbackMap.set(method, [callback]);
+    }
+}
diff --git a/src/screencast/emulatedDevices.ts b/src/screencast/emulatedDevices.ts
new file mode 100644
index 0000000..0ce2176
--- /dev/null
+++ b/src/screencast/emulatedDevices.ts
@@ -0,0 +1,789 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+// Taken from
+// devtools-frontend/src/front_end/models/emulation/EmulatedDevices.ts
+
+export const emulatedDevices = [
+  {
+    'order': 52,
+    'show-by-default': true,
+    'title': 'Nest Hub Max',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/google-nest-hub-max-horizontal.avif)',
+          'insets': {'left': 92, 'top': 96, 'right': 91, 'bottom': 248},
+        },
+        'width': 1280,
+        'height': 800,
+      },
+      'device-pixel-ratio': 2,
+      'vertical': {
+        'width': 1280,
+        'height': 800,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.188 Safari/537.36 CrKey/1.54.250320',
+    'type': 'tablet',
+    'modes': [{'title': 'default', 'orientation': 'horizontal'}],
+  },
+
+  {
+    'order': 50,
+    'show-by-default': true,
+    'title': 'Nest Hub',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/google-nest-hub-horizontal.avif)',
+          'insets': {'left': 82, 'top': 74, 'right': 83, 'bottom': 222},
+        },
+        'width': 1024,
+        'height': 600,
+      },
+      'device-pixel-ratio': 2,
+      'vertical': {
+        'width': 1024,
+        'height': 600,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.109 Safari/537.36 CrKey/1.54.248666',
+    'type': 'tablet',
+    'modes': [{'title': 'default', 'orientation': 'horizontal'}],
+  },
+  {
+    'show-by-default': false,
+    'title': 'iPhone 4',
+    'screen': {
+      'horizontal': {'width': 480, 'height': 320},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 320, 'height': 480},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
+    'type': 'phone',
+  },
+  {
+    'order': 30,
+    'show-by-default': true,
+    'title': 'iPhone 5/SE',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/iPhone5-landscape.avif)',
+          'insets': {'left': 115, 'top': 25, 'right': 115, 'bottom': 28},
+        },
+        'width': 568,
+        'height': 320,
+      },
+      'device-pixel-ratio': 2,
+      'vertical': {
+        'outline': {
+          'image': '@url(optimized/iPhone5-portrait.avif)',
+          'insets': {'left': 29, 'top': 105, 'right': 25, 'bottom': 111},
+        },
+        'width': 320,
+        'height': 568,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+    'type': 'phone',
+  },
+  {
+    'order': 31,
+    'show-by-default': true,
+    'title': 'iPhone 6/7/8',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/iPhone6-landscape.avif)',
+          'insets': {'left': 106, 'top': 28, 'right': 106, 'bottom': 28},
+        },
+        'width': 667,
+        'height': 375,
+      },
+      'device-pixel-ratio': 2,
+      'vertical': {
+        'outline': {
+          'image': '@url(optimized/iPhone6-portrait.avif)',
+          'insets': {'left': 28, 'top': 105, 'right': 28, 'bottom': 105},
+        },
+        'width': 375,
+        'height': 667,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
+    'type': 'phone',
+  },
+  {
+    'order': 32,
+    'show-by-default': true,
+    'title': 'iPhone 6/7/8 Plus',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/iPhone6Plus-landscape.avif)',
+          'insets': {'left': 109, 'top': 29, 'right': 109, 'bottom': 27},
+        },
+        'width': 736,
+        'height': 414,
+      },
+      'device-pixel-ratio': 3,
+      'vertical': {
+        'outline': {
+          'image': '@url(optimized/iPhone6Plus-portrait.avif)',
+          'insets': {'left': 26, 'top': 107, 'right': 30, 'bottom': 111},
+        },
+        'width': 414,
+        'height': 736,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
+    'type': 'phone',
+  },
+  {
+    'order': 33,
+    'show-by-default': true,
+    'title': 'iPhone X',
+    'screen': {
+      'horizontal': {'width': 812, 'height': 375},
+      'device-pixel-ratio': 3,
+      'vertical': {'width': 375, 'height': 812},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'BlackBerry Z30',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 360},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 360, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Nexus 4',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 384},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 384, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '4.4.2', 'architecture': '', 'model': 'Nexus 4', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'title': 'Nexus 5',
+    'type': 'phone',
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '6.0', 'architecture': '', 'model': 'Nexus 5', 'mobile': true},
+    'capabilities': ['touch', 'mobile'],
+    'show-by-default': false,
+    'screen': {
+      'device-pixel-ratio': 3,
+      'vertical': {'width': 360, 'height': 640},
+      'horizontal': {'width': 640, 'height': 360},
+    },
+    'modes': [
+      {
+        'title': 'default',
+        'orientation': 'vertical',
+        'insets': {'left': 0, 'top': 25, 'right': 0, 'bottom': 48},
+        'image':
+            '@url(optimized/google-nexus-5-vertical-default-1x.avif) 1x, @url(optimized/google-nexus-5-vertical-default-2x.avif) 2x',
+      },
+      {
+        'title': 'navigation bar',
+        'orientation': 'vertical',
+        'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 48},
+        'image':
+            '@url(optimized/google-nexus-5-vertical-navigation-1x.avif) 1x, @url(optimized/google-nexus-5-vertical-navigation-2x.avif) 2x',
+      },
+      {
+        'title': 'keyboard',
+        'orientation': 'vertical',
+        'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 312},
+        'image':
+            '@url(optimized/google-nexus-5-vertical-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5-vertical-keyboard-2x.avif) 2x',
+      },
+      {
+        'title': 'default',
+        'orientation': 'horizontal',
+        'insets': {'left': 0, 'top': 25, 'right': 42, 'bottom': 0},
+        'image':
+            '@url(optimized/google-nexus-5-horizontal-default-1x.avif) 1x, @url(optimized/google-nexus-5-horizontal-default-2x.avif) 2x',
+      },
+      {
+        'title': 'navigation bar',
+        'orientation': 'horizontal',
+        'insets': {'left': 0, 'top': 80, 'right': 42, 'bottom': 0},
+        'image':
+            '@url(optimized/google-nexus-5-horizontal-navigation-1x.avif) 1x, @url(optimized/google-nexus-5-horizontal-navigation-2x.avif) 2x',
+      },
+      {
+        'title': 'keyboard',
+        'orientation': 'horizontal',
+        'insets': {'left': 0, 'top': 80, 'right': 42, 'bottom': 202},
+        'image':
+            '@url(optimized/google-nexus-5-horizontal-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5-horizontal-keyboard-2x.avif) 2x',
+      },
+    ],
+  },
+  {
+    'title': 'Nexus 5X',
+    'type': 'phone',
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '8.0.0', 'architecture': '', 'model': 'Nexus 5X', 'mobile': true},
+    'capabilities': ['touch', 'mobile'],
+    'show-by-default': false,
+    'screen': {
+      'device-pixel-ratio': 2.625,
+      'vertical': {
+        'outline': {
+          'image': '@url(optimized/Nexus5X-portrait.avif)',
+          'insets': {'left': 18, 'top': 88, 'right': 22, 'bottom': 98},
+        },
+        'width': 412,
+        'height': 732,
+      },
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/Nexus5X-landscape.avif)',
+          'insets': {'left': 88, 'top': 21, 'right': 98, 'bottom': 19},
+        },
+        'width': 732,
+        'height': 412,
+      },
+    },
+    'modes': [
+      {
+        'title': 'default',
+        'orientation': 'vertical',
+        'insets': {'left': 0, 'top': 24, 'right': 0, 'bottom': 48},
+        'image':
+            '@url(optimized/google-nexus-5x-vertical-default-1x.avif) 1x, @url(optimized/google-nexus-5x-vertical-default-2x.avif) 2x',
+      },
+      {
+        'title': 'navigation bar',
+        'orientation': 'vertical',
+        'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 48},
+        'image':
+            '@url(optimized/google-nexus-5x-vertical-navigation-1x.avif) 1x, @url(optimized/google-nexus-5x-vertical-navigation-2x.avif) 2x',
+      },
+      {
+        'title': 'keyboard',
+        'orientation': 'vertical',
+        'insets': {'left': 0, 'top': 80, 'right': 0, 'bottom': 342},
+        'image':
+            '@url(optimized/google-nexus-5x-vertical-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5x-vertical-keyboard-2x.avif) 2x',
+      },
+      {
+        'title': 'default',
+        'orientation': 'horizontal',
+        'insets': {'left': 0, 'top': 24, 'right': 48, 'bottom': 0},
+        'image':
+            '@url(optimized/google-nexus-5x-horizontal-default-1x.avif) 1x, @url(optimized/google-nexus-5x-horizontal-default-2x.avif) 2x',
+      },
+      {
+        'title': 'navigation bar',
+        'orientation': 'horizontal',
+        'insets': {'left': 0, 'top': 80, 'right': 48, 'bottom': 0},
+        'image':
+            '@url(optimized/google-nexus-5x-horizontal-navigation-1x.avif) 1x, @url(optimized/google-nexus-5x-horizontal-navigation-2x.avif) 2x',
+      },
+      {
+        'title': 'keyboard',
+        'orientation': 'horizontal',
+        'insets': {'left': 0, 'top': 80, 'right': 48, 'bottom': 222},
+        'image':
+            '@url(optimized/google-nexus-5x-horizontal-keyboard-1x.avif) 1x, @url(optimized/google-nexus-5x-horizontal-keyboard-2x.avif) 2x',
+      },
+    ],
+  },
+  {
+    'show-by-default': false,
+    'title': 'Nexus 6',
+    'screen': {
+      'horizontal': {'width': 732, 'height': 412},
+      'device-pixel-ratio': 3.5,
+      'vertical': {'width': 412, 'height': 732},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '7.1.1', 'architecture': '', 'model': 'Nexus 6', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Nexus 6P',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/Nexus6P-landscape.avif)',
+          'insets': {'left': 94, 'top': 17, 'right': 88, 'bottom': 17},
+        },
+        'width': 732,
+        'height': 412,
+      },
+      'device-pixel-ratio': 3.5,
+      'vertical': {
+        'outline': {
+          'image': '@url(optimized/Nexus6P-portrait.avif)',
+          'insets': {'left': 16, 'top': 94, 'right': 16, 'bottom': 88},
+        },
+        'width': 412,
+        'height': 732,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '8.0.0', 'architecture': '', 'model': 'Nexus 6P', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'order': 20,
+    'show-by-default': true,
+    'title': 'Pixel 2',
+    'screen': {
+      'horizontal': {'width': 731, 'height': 411},
+      'device-pixel-ratio': 2.625,
+      'vertical': {'width': 411, 'height': 731},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '8.0', 'architecture': '', 'model': 'Pixel 2', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'order': 21,
+    'show-by-default': true,
+    'title': 'Pixel 2 XL',
+    'screen': {
+      'horizontal': {'width': 823, 'height': 411},
+      'device-pixel-ratio': 3.5,
+      'vertical': {'width': 411, 'height': 823},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '8.0.0', 'architecture': '', 'model': 'Pixel 2 XL', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'LG Optimus L70',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 384},
+      'device-pixel-ratio': 1.25,
+      'vertical': {'width': 384, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '4.4.2', 'architecture': '', 'model': 'LGMS323', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Nokia N9',
+    'screen': {
+      'horizontal': {'width': 854, 'height': 480},
+      'device-pixel-ratio': 1,
+      'vertical': {'width': 480, 'height': 854},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Nokia Lumia 520',
+    'screen': {
+      'horizontal': {'width': 533, 'height': 320},
+      'device-pixel-ratio': 1.5,
+      'vertical': {'width': 320, 'height': 533},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Microsoft Lumia 550',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 360},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 640, 'height': 360},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Microsoft Lumia 950',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 360},
+      'device-pixel-ratio': 4,
+      'vertical': {'width': 360, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Galaxy S III',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 360},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 360, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '4.0', 'architecture': '', 'model': 'GT-I9300', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'order': 10,
+    'show-by-default': true,
+    'title': 'Galaxy S5',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 360},
+      'device-pixel-ratio': 3,
+      'vertical': {'width': 360, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '5.0', 'architecture': '', 'model': 'SM-G900P', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'order': 1,
+    'show-by-default': false,
+    'title': 'JioPhone 2',
+    'screen': {
+      'horizontal': {'width': 320, 'height': 240},
+      'device-pixel-ratio': 1,
+      'vertical': {'width': 240, 'height': 320},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Kindle Fire HDX',
+    'screen': {
+      'horizontal': {'width': 1280, 'height': 800},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 800, 'height': 1280},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
+    'type': 'tablet',
+  },
+  {
+    'show-by-default': false,
+    'title': 'iPad Mini',
+    'screen': {
+      'horizontal': {'width': 1024, 'height': 768},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 768, 'height': 1024},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+    'type': 'tablet',
+  },
+  {
+    'order': 40,
+    'show-by-default': true,
+    'title': 'iPad',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/iPad-landscape.avif)',
+          'insets': {'left': 112, 'top': 56, 'right': 116, 'bottom': 52},
+        },
+        'width': 1024,
+        'height': 768,
+      },
+      'device-pixel-ratio': 2,
+      'vertical': {
+        'outline': {
+          'image': '@url(optimized/iPad-portrait.avif)',
+          'insets': {'left': 52, 'top': 114, 'right': 55, 'bottom': 114},
+        },
+        'width': 768,
+        'height': 1024,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+    'type': 'tablet',
+  },
+  {
+    'order': 41,
+    'show-by-default': true,
+    'title': 'iPad Pro',
+    'screen': {
+      'horizontal': {'width': 1366, 'height': 1024},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 1024, 'height': 1366},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+    'type': 'tablet',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Blackberry PlayBook',
+    'screen': {
+      'horizontal': {'width': 1024, 'height': 600},
+      'device-pixel-ratio': 1,
+      'vertical': {'width': 600, 'height': 1024},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
+    'type': 'tablet',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Nexus 10',
+    'screen': {
+      'horizontal': {'width': 1280, 'height': 800},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 800, 'height': 1280},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '6.0.1', 'architecture': '', 'model': 'Nexus 10', 'mobile': false},
+    'type': 'tablet',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Nexus 7',
+    'screen': {
+      'horizontal': {'width': 960, 'height': 600},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 600, 'height': 960},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '6.0.1', 'architecture': '', 'model': 'Nexus 7', 'mobile': false},
+    'type': 'tablet',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Galaxy Note 3',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 360},
+      'device-pixel-ratio': 3,
+      'vertical': {'width': 360, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '4.3', 'architecture': '', 'model': 'SM-N900T', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Galaxy Note II',
+    'screen': {
+      'horizontal': {'width': 640, 'height': 360},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 360, 'height': 640},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '4.1', 'architecture': '', 'model': 'GT-N7100', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'show-by-default': false,
+    'title': 'Laptop with touch',
+    'screen': {
+      'horizontal': {'width': 1280, 'height': 950},
+      'device-pixel-ratio': 1,
+      'vertical': {'width': 950, 'height': 1280},
+    },
+    'capabilities': ['touch'],
+    'user-agent': '',
+    'type': 'notebook',
+    'modes': [{'title': 'default', 'orientation': 'horizontal'}],
+  },
+  {
+    'show-by-default': false,
+    'title': 'Laptop with HiDPI screen',
+    'screen': {
+      'horizontal': {'width': 1440, 'height': 900},
+      'device-pixel-ratio': 2,
+      'vertical': {'width': 900, 'height': 1440},
+    },
+    'capabilities': [],
+    'user-agent': '',
+    'type': 'notebook',
+    'modes': [{'title': 'default', 'orientation': 'horizontal'}],
+  },
+  {
+    'show-by-default': false,
+    'title': 'Laptop with MDPI screen',
+    'screen': {
+      'horizontal': {'width': 1280, 'height': 800},
+      'device-pixel-ratio': 1,
+      'vertical': {'width': 800, 'height': 1280},
+    },
+    'capabilities': [],
+    'user-agent': '',
+    'type': 'notebook',
+    'modes': [{'title': 'default', 'orientation': 'horizontal'}],
+  },
+  {
+    'show-by-default': true,
+    'title': 'Moto G4',
+    'screen': {
+      'horizontal': {
+        'outline': {
+          'image': '@url(optimized/MotoG4-landscape.avif)',
+          'insets': {'left': 91, 'top': 30, 'right': 74, 'bottom': 30},
+        },
+        'width': 640,
+        'height': 360,
+      },
+      'device-pixel-ratio': 3,
+      'vertical': {
+        'outline': {
+          'image': '@url(optimized/MotoG4-portrait.avif)',
+          'insets': {'left': 30, 'top': 91, 'right': 30, 'bottom': 74},
+        },
+        'width': 360,
+        'height': 640,
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 6.0.1; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'user-agent-metadata':
+        {'platform': 'Android', 'platformVersion': '6.0.1', 'architecture': '', 'model': 'Moto G (4)', 'mobile': true},
+    'type': 'phone',
+  },
+  {
+    'order': 45,
+    'show-by-default': true,
+    'dual-screen': true,
+    'title': 'Surface Duo',
+    'screen': {
+      'horizontal': {'width': 720, 'height': 540},
+      'device-pixel-ratio': 2.5,
+      'vertical': {'width': 540, 'height': 720},
+      'vertical-spanned': {
+        'width': 1114,
+        'height': 720,
+        'hinge': {'width': 34, 'height': 720, 'x': 540, 'y': 0, 'contentColor': {'r': 38, 'g': 38, 'b': 38, 'a': 1}},
+      },
+      'horizontal-spanned': {
+        'width': 720,
+        'height': 1114,
+        'hinge': {'width': 720, 'height': 34, 'x': 0, 'y': 540, 'contentColor': {'r': 38, 'g': 38, 'b': 38, 'a': 1}},
+      },
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'type': 'phone',
+    'modes': [
+      {'title': 'default', 'orientation': 'vertical', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
+      {'title': 'default', 'orientation': 'horizontal', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
+      {'title': 'spanned', 'orientation': 'vertical-spanned', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
+      {
+        'title': 'spanned',
+        'orientation': 'horizontal-spanned',
+        'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0},
+      },
+    ],
+  },
+  {
+    'order': 46,
+    'show-by-default': true,
+    'dual-screen': true,
+    'title': 'Galaxy Fold',
+    'screen': {
+      'horizontal': {'width': 653, 'height': 280},
+      'device-pixel-ratio': 3,
+      'vertical': {'width': 280, 'height': 653},
+      'vertical-spanned': {'width': 717, 'height': 512},
+      'horizontal-spanned': {'width': 512, 'height': 717},
+    },
+    'capabilities': ['touch', 'mobile'],
+    'user-agent':
+        'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
+    'type': 'phone',
+    'modes': [
+      {'title': 'default', 'orientation': 'vertical', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
+      {'title': 'default', 'orientation': 'horizontal', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
+      {'title': 'spanned', 'orientation': 'vertical-spanned', 'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0}},
+      {
+        'title': 'spanned',
+        'orientation': 'horizontal-spanned',
+        'insets': {'left': 0, 'top': 0, 'right': 0, 'bottom': 0},
+      },
+    ],
+  },
+];
\ No newline at end of file
diff --git a/src/screencast/input.ts b/src/screencast/input.ts
new file mode 100644
index 0000000..228d367
--- /dev/null
+++ b/src/screencast/input.ts
@@ -0,0 +1,118 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { ScreencastCDPConnection } from "./cdp";
+
+export const MouseEventMap = {
+  mousedown: 'mousePressed',
+  mouseup: 'mouseReleased',
+  mousemove: 'mouseMoved',
+  wheel: 'mouseWheel'
+};
+
+const MouseButtonMap = [
+    'left',
+    'middle',
+    'right',
+    'back',
+    'forward'
+];
+
+export class ScreencastInputHandler {
+    private cdpConnection: ScreencastCDPConnection;
+    private activeTouchParams: any | null;
+
+    constructor(cdpConnection: ScreencastCDPConnection) {
+        this.cdpConnection = cdpConnection;
+        this.activeTouchParams = null;
+    }
+
+    emitMouseEvent(mouseEvent: MouseEvent, scale: number): void {
+        const eventType = MouseEventMap[mouseEvent.type as keyof typeof MouseEventMap];
+        if (!eventType) {
+            return;
+        }
+        this.cdpConnection.sendMessageToBackend('Input.dispatchMouseEvent', {
+            type: eventType,
+            clickCount: eventType === 'mousePressed' || eventType === 'mouseReleased' ? 1 : 0,
+            x: Math.round(mouseEvent.offsetX / scale),
+            y: Math.round(mouseEvent.offsetY / scale),
+            modifiers: this.modifiersForEvent(mouseEvent),
+            button: MouseButtonMap[mouseEvent.button],
+            buttons: mouseEvent.buttons,
+            deltaX: (mouseEvent as WheelEvent).deltaX,
+            deltaY: (mouseEvent as WheelEvent).deltaY
+        });
+    }
+
+    emitKeyEvent(keyboardEvent: KeyboardEvent): void {
+        // For what seems a bug to me on CDP:
+        // - non printable key events only respond to object with type keydown and virtual key codes.
+        // - printable characters respond only to object with type char and text property set to key.
+        // This could be related:
+        // https://github.com/ChromeDevTools/devtools-protocol/issues/45
+        if(keyboardEvent.type === 'keydown' && keyboardEvent.key.length > 1 && keyboardEvent.key !== 'Enter') {
+            this.cdpConnection.sendMessageToBackend('Input.dispatchKeyEvent', {
+                type: 'keyDown',
+                windowsVirtualKeyCode: keyboardEvent.keyCode,
+                nativeVirtualKeyCode: keyboardEvent.keyCode,
+            });
+        } else if(keyboardEvent.type === 'keypress') {
+            const cdpObject = { 
+                type: 'char',
+                text: keyboardEvent.key
+            }
+
+            if (keyboardEvent.key === 'Enter') {
+                cdpObject.text = '\r';
+            }
+
+            this.cdpConnection.sendMessageToBackend('Input.dispatchKeyEvent', cdpObject);
+        }
+    }
+
+    emitTouchFromMouseEvent(mouseEvent: MouseEvent, scale: number): void {
+        const buttons = ['none', 'left', 'middle', 'right'];
+        const eventType = MouseEventMap[mouseEvent.type as keyof typeof MouseEventMap];
+        if (!eventType) {
+            return;
+        }
+
+        if (!(mouseEvent.which in buttons)) {
+            return;
+        }
+        if (eventType !== 'wheel' && buttons[mouseEvent.which] === 'none') {
+            return;
+        }
+
+        const params: any = {
+            type: eventType,
+            x: Math.round(mouseEvent.offsetX / scale),
+            y: Math.round(mouseEvent.offsetY / scale),
+            modifiers: 0,
+            button: MouseButtonMap[mouseEvent.button],
+            clickCount: 0,
+        };
+        if (mouseEvent.type === 'wheel') {
+            const wheelEvent = mouseEvent as WheelEvent;
+            params.deltaX = wheelEvent.deltaX;
+            params.deltaY = -wheelEvent.deltaY;
+        } else {
+            this.activeTouchParams = params;
+        }
+        this.cdpConnection.sendMessageToBackend('Input.emulateTouchFromMouseEvent', params);
+    }
+
+    cancelTouch(): void {
+        if (this.activeTouchParams !== null) {
+            const params = this.activeTouchParams;
+            this.activeTouchParams = null;
+            params.type = 'mouseReleased';
+            this.cdpConnection.sendMessageToBackend('Input.emulateTouchFromMouseEvent', params);
+        }
+    }
+
+    private modifiersForEvent(event: MouseEvent | KeyboardEvent): number {
+        return (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
+    }
+}
diff --git a/src/screencast/main.ts b/src/screencast/main.ts
new file mode 100644
index 0000000..34b4c08
--- /dev/null
+++ b/src/screencast/main.ts
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { Screencast } from './screencast';
+
+new Screencast();
diff --git a/src/screencast/screencast.ts b/src/screencast/screencast.ts
new file mode 100644
index 0000000..6aa99cc
--- /dev/null
+++ b/src/screencast/screencast.ts
@@ -0,0 +1,217 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { ErrorCodes } from '../common/errorCodes';
+import { encodeMessageForChannel, TelemetryData } from '../common/webviewEvents';
+import { ScreencastCDPConnection, vscode } from './cdp';
+import { MouseEventMap, ScreencastInputHandler } from './input';
+
+type NavigationEntry = {
+    id: number;
+    url: string;
+};
+
+export class Screencast {
+    private cdpConnection = new ScreencastCDPConnection();
+    private history: NavigationEntry[] = [];
+    private historyIndex = 0;
+    private inputHandler: ScreencastInputHandler;
+    private backButton: HTMLButtonElement;
+    private forwardButton: HTMLButtonElement;
+    private reloadButton: HTMLButtonElement;
+    private rotateButton: HTMLButtonElement;
+    private urlInput: HTMLInputElement;
+    private screencastImage: HTMLImageElement;
+    private screencastWrapper: HTMLElement;
+    private deviceSelect: HTMLSelectElement;
+    private width = 0;
+    private height = 0;
+
+    constructor() {
+        this.backButton = document.getElementById('back') as HTMLButtonElement;
+        this.forwardButton = document.getElementById('forward') as HTMLButtonElement;
+        this.reloadButton = document.getElementById('reload') as HTMLButtonElement;
+        this.rotateButton = document.getElementById('rotate') as HTMLButtonElement;
+        this.urlInput = document.getElementById('url') as HTMLInputElement;
+        this.screencastImage = document.getElementById('canvas') as HTMLImageElement;
+        this.screencastWrapper = document.getElementById('canvas-wrapper') as HTMLElement;
+        this.deviceSelect = document.getElementById('device') as HTMLSelectElement;
+
+        this.backButton.addEventListener('click', () => this.onBackClick());
+        this.forwardButton.addEventListener('click', () => this.onForwardClick());
+        this.reloadButton.addEventListener('click', () => this.onReloadClick());
+        this.rotateButton.addEventListener('click', () => this.onRotateClick());
+        this.urlInput.addEventListener('keydown', event => this.onUrlKeyDown(event));
+
+        this.deviceSelect.addEventListener('change', () => {
+            if (this.deviceSelect.value.toLowerCase() === 'desktop') {
+                this.width = 0;
+                this.height = 0;
+                this.screencastWrapper.classList.add('desktop');
+            } else {
+                const selectedOption = this.deviceSelect[this.deviceSelect.selectedIndex];
+                const deviceWidth = selectedOption.getAttribute('devicewidth');
+                const deviceHeight = selectedOption.getAttribute('deviceheight');
+                if (deviceWidth && deviceHeight) {
+                    this.width = parseInt(deviceWidth);
+                    this.height = parseInt(deviceHeight);
+                } else {
+                    this.reportError(ErrorCodes.Error, 'Error while getting screencast width and height.', `Actual width: ${deviceWidth}, height: ${deviceHeight}`);
+                }
+
+                this.screencastWrapper.classList.remove('desktop');
+            }
+            this.updateEmulation();
+        });
+
+        this.cdpConnection.registerForEvent('Page.domContentEventFired', () => this.onDomContentEventFired());
+        this.cdpConnection.registerForEvent('Page.frameNavigated', result => this.onFrameNavigated(result));
+        this.cdpConnection.registerForEvent('Page.screencastFrame', result => this.onScreencastFrame(result));
+
+        this.inputHandler = new ScreencastInputHandler(this.cdpConnection);
+
+        this.cdpConnection.sendMessageToBackend('Page.enable', {});
+
+        // Optimizing the resize event to limit how often can it be called.
+        let resizeTimeout = 0 as unknown as NodeJS.Timeout;
+        window.addEventListener('resize', () => {
+            clearTimeout(resizeTimeout);
+            resizeTimeout = setTimeout(() => this.updateEmulation(), 100);
+        });
+
+        this.registerInputListeners();
+        
+        // Start screencast
+        this.updateEmulation();
+        this.updateHistory();
+    }
+
+    private registerInputListeners(): void {
+        for (const eventName of Object.keys(MouseEventMap)) {
+            this.screencastImage.addEventListener(eventName, (event) => {
+                const scale = this.screencastImage.offsetWidth / this.screencastImage.naturalWidth;
+                if (this.isDeviceTouch()) {
+                    this.inputHandler.emitTouchFromMouseEvent(event as MouseEvent, scale);
+                } else {
+                    this.inputHandler.emitMouseEvent(event as MouseEvent, scale);
+                }
+            });
+        }
+
+        for (const eventName of ['keydown', 'keypress']) {
+            this.screencastImage.addEventListener(eventName, (event) => {
+                this.inputHandler.emitKeyEvent(event as KeyboardEvent);
+            });
+        }
+    }
+
+    private updateHistory(): void {
+        this.cdpConnection.sendMessageToBackend('Page.getNavigationHistory', {}, (result) => {
+            const {currentIndex, entries} = result;
+            this.history = entries;
+            this.historyIndex = currentIndex;
+            this.backButton.disabled = this.historyIndex < 1;
+            this.forwardButton.disabled = this.historyIndex >= this.history.length - 1;
+            this.urlInput.value = this.history[this.historyIndex].url;
+        });
+    }
+
+    private updateEmulation(): void {
+        const params = {
+            width: this.width || this.screencastWrapper.offsetWidth,
+            height: this.height || this.screencastWrapper.offsetHeight,
+            deviceScaleFactor: 0,
+            mobile: this.isDeviceTouch()
+        }
+        this.cdpConnection.sendMessageToBackend('Emulation.setDeviceMetricsOverride', params);
+        this.updateScreencast();
+    }
+
+    private reportError(type: ErrorCodes.Error, message: string, stack: string) {
+
+        // Package up the error info to send to the extension
+        const data = { type, message, stack };
+
+        // Inform the extension of the DevTools telemetry event
+        this.sendTelemetry({
+            data,
+            event: 'error',
+            name: 'screencast error',
+        });
+    }
+
+    private sendTelemetry(telemetry: TelemetryData) {
+        // Forward the data to the extension
+        encodeMessageForChannel(msg => vscode.postMessage(msg, '*'), 'telemetry', telemetry);
+    }
+
+    private isDeviceTouch(){
+        const selectedOption = this.deviceSelect[this.deviceSelect.selectedIndex];
+        return selectedOption.getAttribute('touch') === "true" 
+            || selectedOption.getAttribute('mobile') === "true";
+    }
+
+    private updateScreencast(): void {
+        const screencastParams = {
+            format: 'png',
+            quality: 100,
+            maxWidth: this.width || this.screencastWrapper.offsetWidth,
+            maxHeight: this.height || this.screencastWrapper.offsetHeight
+        };
+        this.cdpConnection.sendMessageToBackend('Page.startScreencast', screencastParams);
+    }
+
+    private onBackClick(): void {
+        if (this.historyIndex > 0) {
+            const entryId = this.history[this.historyIndex - 1].id;
+            this.cdpConnection.sendMessageToBackend('Page.navigateToHistoryEntry', {entryId})
+        }
+    }
+
+    private onDomContentEventFired(): void {
+        setTimeout(() => this.updateEmulation(), 100);
+    }
+
+    private onForwardClick(): void {
+        if (this.historyIndex < this.history.length - 1) {
+            const entryId = this.history[this.historyIndex + 1].id;
+            this.cdpConnection.sendMessageToBackend('Page.navigateToHistoryEntry', {entryId})
+        }
+    }
+
+    private onFrameNavigated({frame}: any): void {
+        if (!frame.parentId) {
+            this.updateHistory();
+            this.updateEmulation();
+        }
+    }
+
+    private onReloadClick(): void {
+        this.cdpConnection.sendMessageToBackend('Page.reload', {});
+    }
+
+    private onRotateClick(): void {
+        const width = this.height;
+        const height = this.width;
+        this.width = width;
+        this.height = height;
+        this.updateEmulation();
+    }
+
+    private onUrlKeyDown(event: KeyboardEvent): void {
+        let url = this.urlInput.value;
+        if (event.key === 'Enter' && url) {
+            if (!url.startsWith('http') || !url.startsWith('file')) {
+                url = 'http://' + url;
+            }
+
+            this.cdpConnection.sendMessageToBackend('Page.navigate', {url});
+        }
+    }
+
+    private onScreencastFrame({data, sessionId}: any): void {
+        this.screencastImage.src = 'data:image/png;base64,' + data;
+        this.screencastImage.style.width = `${this.screencastImage.naturalWidth}px`;
+        this.cdpConnection.sendMessageToBackend('Page.screencastFrameAck', {sessionId});
+    }
+}
diff --git a/src/screencast/view.css b/src/screencast/view.css
new file mode 100644
index 0000000..5aa369f
--- /dev/null
+++ b/src/screencast/view.css
@@ -0,0 +1,56 @@
+html,
+body {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+#main {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+}
+
+#toolbar {
+  display: flex;
+  width: 100%;
+}
+
+#toolbar>button,
+#toolbar>select {
+  background-color: var(--vscode-editor-background);
+  border: none;
+  color: var(--vscode-editor-foreground);
+}
+
+#url {
+  flex: 1;
+  background-color: var(--vscode-input-background);
+  border-style: solid;
+  border-width: 1px;
+  color: var(--vscode-input-foreground);
+}
+
+#canvas-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+}
+
+#canvas-wrapper.fill #canvas {
+  max-width: none;
+  max-height: none;
+}
+
+#canvas {
+  outline: none;
+}
\ No newline at end of file
diff --git a/src/screencast/view.ts b/src/screencast/view.ts
new file mode 100644
index 0000000..56dbeaf
--- /dev/null
+++ b/src/screencast/view.ts
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { Uri } from 'vscode';
+import { emulatedDevices } from "./emulatedDevices";
+
+export class ScreencastView {
+    private webviewCSP: string;
+    private cssPath: Uri
+    private codiconsUri: Uri;
+    private inspectorUri: Uri
+    private htmlTemplate = (webviewCSP: string, cssPath: Uri, codiconsUri: Uri, inspectorUri: Uri, deviceList: string) => `<!doctype html>
+  <html>
+  <head>
+      <meta http-equiv="content-type" content="text/html; charset=utf-8">
+      <meta http-equiv="Content-Security-Policy"
+          content="default-src;
+          font-src ${webviewCSP};
+          img-src 'self' data: ${webviewCSP};
+          style-src 'self' 'unsafe-inline' ${webviewCSP};
+          script-src 'self' 'unsafe-eval' ${webviewCSP};
+          frame-src 'self' ${webviewCSP};
+          connect-src 'self' data: ${webviewCSP};
+      ">
+      <meta name="referrer" content="no-referrer">
+      <link href="${codiconsUri}" rel="stylesheet" />
+      <link href="${cssPath}" rel="stylesheet" />
+      <script type="module" src="${inspectorUri}"></script>
+  </head>
+  <body>
+      <div id="main">
+          <div id="toolbar">
+              <button id="back">
+                  <i class="codicon codicon-arrow-left"></i>
+              </button>
+              <button id="forward">
+                  <i class="codicon codicon-arrow-right"></i>
+              </button>
+              <button id="reload">
+                  <i class="codicon codicon-refresh"></i>
+              </button>
+              <input id="url" />
+              <select id="device">
+                  <option selected value="desktop">Desktop</option>
+                  ${deviceList}
+              </select>
+              <button id="rotate">
+                  <i class="codicon codicon-editor-layout"></i>
+              </button>
+          </div>
+          <div id="canvas-wrapper" class="fill">
+              <img id="canvas" draggable="false" tabindex="0" />
+          </div>
+      </div>
+  </body>
+  </html>
+  `;
+
+    constructor(webviewCSP: string, cssPath: Uri, codiconsUri: Uri, inspectorUri: Uri) {
+        this.webviewCSP = webviewCSP;
+        this.cssPath = cssPath;
+        this.codiconsUri = codiconsUri;
+        this.inspectorUri = inspectorUri;
+    }
+
+    private getDeviceList(devicesArray: Object[]) {
+        let templatedString = '';
+
+        for (const device of devicesArray) {
+            // @ts-ignore ignoring as this is a static template.
+            templatedString += `<option deviceWidth=${device.screen.vertical.width} deviceHeight=${device.screen.vertical.height} ${ScreencastView.getDeviceCapabilities(device.capabilities)} value="${ScreencastView.getDeviceValueFromTitle(device.title)}">${device.title}</option>`;
+        }
+
+        return templatedString;
+    }
+
+    public static getDeviceCapabilities(deviceCapabilities: string[]){
+        let result = '';
+        for(const device of deviceCapabilities){
+            if(device === 'touch' || device === 'mobile') {
+                result += `${device}=true `
+            }
+        }
+
+        return result;
+    }
+
+    public static getDeviceValueFromTitle(title: string) {
+        return title.replace(/['/' || ' ' || '-']/g, '');
+    }
+
+    public render(): string {
+        const deviceList = this.getDeviceList(emulatedDevices);
+        return this.htmlTemplate(this.webviewCSP, this.cssPath, this.codiconsUri, this.inspectorUri, deviceList);
+    }
+}
diff --git a/src/screencastPanel.ts b/src/screencastPanel.ts
new file mode 100644
index 0000000..d7e8508
--- /dev/null
+++ b/src/screencastPanel.ts
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+import * as path from 'path';
+import * as vscode from 'vscode';
+import { encodeMessageForChannel, WebSocketEvent } from './common/webviewEvents';
+import { PanelSocket } from './panelSocket';
+import { ScreencastView } from './screencast/view';
+import {
+    SETTINGS_STORE_NAME,
+    SETTINGS_SCREENCAST_WEBVIEW_NAME,
+} from './utils';
+
+export class ScreencastPanel {
+    private static instance: ScreencastPanel | undefined;
+    private readonly context: vscode.ExtensionContext;
+    private readonly extensionPath: string;
+    private readonly panel: vscode.WebviewPanel;
+    private targetUrl: string
+    private panelSocket: PanelSocket;
+
+    private constructor(
+        panel: vscode.WebviewPanel,
+        context: vscode.ExtensionContext,
+        targetUrl: string) {
+        this.panel = panel;
+        this.context = context;
+        this.targetUrl = targetUrl;
+        this.extensionPath = this.context.extensionPath;
+
+        this.panelSocket = new PanelSocket(this.targetUrl, (e, msg) => this.postToWebview(e, msg));
+        this.panelSocket.on('close', () => this.onSocketClose());
+
+        // Handle closing
+        this.panel.onDidDispose(() => {
+            this.dispose();
+            this.panelSocket.dispose();
+        }, this);
+
+        // Handle view change
+        this.panel.onDidChangeViewState(_e => {
+            if (this.panel.visible) {
+                this.update();
+            }
+        }, this);
+
+        // Handle messages from the webview
+        this.panel.webview.onDidReceiveMessage(message => {
+            this.panelSocket.onMessageFromWebview(message);
+        }, this);
+    }
+
+    dispose(): void {
+        ScreencastPanel.instance = undefined;
+
+        this.panel.dispose();
+        this.panelSocket.dispose();
+    }
+
+    private onSocketClose() {
+        this.dispose();
+    }
+
+    private update() {
+        this.panel.webview.html = this.getHtmlForWebview();
+    }
+
+    private postToWebview(e: WebSocketEvent, message?: string) {
+        encodeMessageForChannel(msg => this.panel.webview.postMessage(msg) as unknown as void, 'websocket', { event: e, message });
+    }
+
+    private getHtmlForWebview() {
+        const inspectorPath = vscode.Uri.file(path.join(this.extensionPath, 'out/screencast', 'screencast.bundle.js'));
+        const inspectorUri = this.panel.webview.asWebviewUri(inspectorPath);
+		const codiconsUri = this.panel.webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'node_modules', '@vscode/codicons', 'dist', 'codicon.css'));
+        const cssPath = this.panel.webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'out/screencast', 'view.css'));
+        const view = new ScreencastView(this.panel.webview.cspSource, cssPath, codiconsUri, inspectorUri);
+        return view.render();
+    }
+
+    static createOrShow(context: vscode.ExtensionContext,
+        targetUrl: string): void {
+        const column = vscode.ViewColumn.Beside;
+        if (ScreencastPanel.instance) {
+            ScreencastPanel.instance.dispose();
+        } else {
+            const panel = vscode.window.createWebviewPanel(SETTINGS_STORE_NAME, SETTINGS_SCREENCAST_WEBVIEW_NAME, column, {
+                enableCommandUris: true,
+                enableScripts: true,
+                retainContextWhenHidden: true,
+            });
+            ScreencastPanel.instance = new ScreencastPanel(panel, context, targetUrl);
+        }
+    }
+}
diff --git a/src/utils.ts b/src/utils.ts
index fd4072b..17b7f40 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -14,7 +14,8 @@ import packageJson from '../package.json';
 import { DebugTelemetryReporter } from './debugTelemetryReporter';
 
 import puppeteer from 'puppeteer-core';
-import { ErrorCodes, ErrorReporter } from './errorReporter';
+import { ErrorReporter } from './errorReporter';
+import { ErrorCodes } from './common/errorCodes';
 
 export type BrowserFlavor = 'Default' | 'Stable' | 'Beta' | 'Dev' | 'Canary';
 
@@ -81,6 +82,7 @@ export const SETTINGS_DEFAULT_HOSTNAME = 'localhost';
 export const SETTINGS_DEFAULT_PORT = 9222;
 export const SETTINGS_DEFAULT_URL = path.resolve(path.join(__dirname, 'startpage', 'index.html'));
 export const SETTINGS_WEBVIEW_NAME = 'Edge DevTools';
+export const SETTINGS_SCREENCAST_WEBVIEW_NAME = 'Edge DevTools: Screencast';
 export const SETTINGS_PREF_NAME = 'devtools-preferences';
 export const SETTINGS_PREF_DEFAULTS = {
     screencastEnabled: false,
@@ -137,6 +139,7 @@ export const buttonCode: Record<string, string> = {
     viewChangelog: '8',
     closeTarget: '9',
     emptyTargetListLaunchBrowserInstance: '10',
+    toggleScreencast: '10',
 };
 
 /**
diff --git a/webpack.config.ts b/webpack.config.ts
index a5f6ab7..feddf25 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -32,6 +32,17 @@ module.exports = (env: any) => {
                 path: path.resolve(__dirname, 'out/host'),
             },
         },
+        {
+            ...commonConfig,
+            entry: {
+                screencast: './src/screencast/main.ts',
+            },
+            name: 'screencast',
+            output: {
+                filename: '[name].bundle.js',
+                path: path.resolve(__dirname, 'out/screencast'),
+            },
+        },
         {
             ...commonConfig,
             entry: {
@@ -66,6 +77,7 @@ module.exports = (env: any) => {
                     patterns: [
                     { from: 'startpage', to: 'startpage'},
                     { from: 'icon.png', to: 'icon.png'},
+                    { from: 'src/screencast/view.css', to: 'screencast/view.css'},
                     ],
                 }),
                 // These must also be defined in the jest section of package.json for tests to pass


Test patch:
diff --git a/test/extension.test.ts b/test/extension.test.ts
index 2acc010..bf5c36a 100644
--- a/test/extension.test.ts
+++ b/test/extension.test.ts
@@ -81,8 +81,8 @@ describe("extension", () => {
             // Activation should add the commands as subscriptions on the context
             newExtension.activate(context);
 
-            expect(context.subscriptions.length).toBe(16);
-            expect(commandMock).toHaveBeenCalledTimes(15);
+            expect(context.subscriptions.length).toBe(17);
+            expect(commandMock).toHaveBeenCalledTimes(16);
             expect(commandMock)
                 .toHaveBeenNthCalledWith(1, `${SETTINGS_STORE_NAME}.attach`, expect.any(Function));
             expect(commandMock)
@@ -96,19 +96,21 @@ describe("extension", () => {
             expect(commandMock)
                 .toHaveBeenNthCalledWith(6, `${SETTINGS_VIEW_NAME}.attach`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(7, `${SETTINGS_VIEW_NAME}.openSettings`, expect.any(Function));
+                .toHaveBeenNthCalledWith(7, `${SETTINGS_VIEW_NAME}.toggleScreencast`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(8, `${SETTINGS_VIEW_NAME}.viewChangelog`, expect.any(Function));
+                .toHaveBeenNthCalledWith(8, `${SETTINGS_VIEW_NAME}.openSettings`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(9, `${SETTINGS_VIEW_NAME}.close-instance`, expect.any(Function));
+                .toHaveBeenNthCalledWith(9, `${SETTINGS_VIEW_NAME}.viewChangelog`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(10, `${SETTINGS_VIEW_NAME}.copyItem`, expect.any(Function));
+                .toHaveBeenNthCalledWith(10, `${SETTINGS_VIEW_NAME}.close-instance`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(11, `${SETTINGS_VIEW_NAME}.configureLaunchJson`, expect.any(Function));
+                .toHaveBeenNthCalledWith(11, `${SETTINGS_VIEW_NAME}.copyItem`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(12, `${SETTINGS_VIEW_NAME}.launchProject`, expect.any(Function));
+                .toHaveBeenNthCalledWith(12, `${SETTINGS_VIEW_NAME}.configureLaunchJson`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(13, `${SETTINGS_VIEW_NAME}.viewDocumentation`, expect.any(Function));
+                .toHaveBeenNthCalledWith(13, `${SETTINGS_VIEW_NAME}.launchProject`, expect.any(Function));
+            expect(commandMock)
+                .toHaveBeenNthCalledWith(14, `${SETTINGS_VIEW_NAME}.viewDocumentation`, expect.any(Function));
             expect(mockRegisterTree)
                 .toHaveBeenNthCalledWith(1, `${SETTINGS_VIEW_NAME}.targets`, expect.any(Object));
         });
@@ -165,7 +167,7 @@ describe("extension", () => {
             attach.callback.call(attach.thisObj, { websocketUrl: "" });
             expect(mockPanelShow).toHaveBeenCalled();
 
-            const copy = getCommandCallback(9);
+            const copy = getCommandCallback(10);
             copy.callback.call(copy.thisObj, { tooltip: "something" });
             expect(mockClipboard).toHaveBeenCalledWith("something");
         });