From 704babea39e88b296e770daf36ee2252a386e5bc Mon Sep 17 00:00:00 2001
From: Bjoern Esswein <bjoern.esswein@gmail.com>
Date: Sun, 16 Jul 2023 22:35:00 +0200
Subject: [PATCH] split BrowserView.cs into separate classes

---
 Runtime/BrowserView.cs                        | 364 +-----------------
 Runtime/ChromeDevtools.meta                   |   8 +
 Runtime/ChromeDevtools/Browser.cs             | 146 +++++++
 Runtime/ChromeDevtools/Browser.cs.meta        |  11 +
 Runtime/ChromeDevtools/BrowserTab.cs          | 134 +++++++
 Runtime/ChromeDevtools/BrowserTab.cs.meta     |  11 +
 Runtime/ChromeDevtools/PageTargetInfo.cs      |  16 +
 Runtime/ChromeDevtools/PageTargetInfo.cs.meta |  11 +
 Runtime/ChromeDevtools/Protocol.meta          |   8 +
 .../Protocol/CaptureScreenshot.cs             |  47 +++
 .../Protocol/CaptureScreenshot.cs.meta        |  11 +
 .../Protocol/DevtoolsCommand.cs               |  25 ++
 .../Protocol/DevtoolsCommand.cs.meta          |  11 +
 Runtime/ChromeDevtools/Protocol/Target.meta   |   8 +
 .../Protocol/Target/CloseTarget.cs            |  34 ++
 .../Protocol/Target/CloseTarget.cs.meta       |  11 +
 Runtime/ChromeDevtools/Protocol/Types.meta    |   8 +
 .../ChromeDevtools/Protocol/Types/Viewport.cs |  35 ++
 .../Protocol/Types/Viewport.cs.meta           |  11 +
 19 files changed, 566 insertions(+), 344 deletions(-)
 create mode 100644 Runtime/ChromeDevtools.meta
 create mode 100644 Runtime/ChromeDevtools/Browser.cs
 create mode 100644 Runtime/ChromeDevtools/Browser.cs.meta
 create mode 100644 Runtime/ChromeDevtools/BrowserTab.cs
 create mode 100644 Runtime/ChromeDevtools/BrowserTab.cs.meta
 create mode 100644 Runtime/ChromeDevtools/PageTargetInfo.cs
 create mode 100644 Runtime/ChromeDevtools/PageTargetInfo.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/Target.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/Types.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/Types/Viewport.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/Types/Viewport.cs.meta

diff --git a/Runtime/BrowserView.cs b/Runtime/BrowserView.cs
index bc6111f..bb78112 100644
--- a/Runtime/BrowserView.cs
+++ b/Runtime/BrowserView.cs
@@ -1,35 +1,12 @@
-using ChromeDevToolsProtocol;
-using ChromeDevToolsProtocol.WebSocketProtocol;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using Newtonsoft.Json.Serialization;
-using System;
-using System.IO;
-using System.Text;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Collections;
-using System.Diagnostics;
+using ChromeDevTools;
 using UnityEngine;
-using UnityEngine.Networking;
 using UnityEngine.UI;
 //using Unity.Networking.Transport;
 
 public class BrowserView : MonoBehaviour
 {
-    // each instance of this class gets its own tab in this browser
-    private static Process browserProcess;
-    private const string browserExecutablePath = "chrome";
-    private const bool headlessBrowser = false;
-    private const int debugPort = 9222;
-
-    private ClientWebSocket ws = new ClientWebSocket();
-    private PageTargetInfo pageTarget;
-
-    private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
-    private JsonSerializer serializer;
-    private JsonSerializerSettings serializerSettings;
+    private Browser browser;
+    private BrowserTab tab;
 
     private RawImage rawImage;
 
@@ -43,347 +20,46 @@ public class BrowserView : MonoBehaviour
 
     void OnEnable()
     {
-        var camelCasePropertyNamesContractResolver = new CamelCasePropertyNamesContractResolver();
-        serializer = new JsonSerializer();
-        serializer.ContractResolver = camelCasePropertyNamesContractResolver;
-        serializerSettings = new JsonSerializerSettings();
-        serializerSettings.ContractResolver = camelCasePropertyNamesContractResolver;
-
-        BrowserView.LaunchBrowser();
+        browser = Browser.getInstance();
+        //BrowserView.LaunchBrowser();
 
-        UnityEngine.Debug.Log($"ws State:\n{ws.State}");
+        //UnityEngine.Debug.Log($"ws State:\n{ws.State}");
         //StartCoroutine(GetOpenTabs());
-        var c = StartCoroutine(OpenNewTab(delegate
+        var c = StartCoroutine(browser.OpenNewTab(targetUrl, (BrowserTab bt) =>
         {
-            UnityEngine.Debug.Log($"ws ConnectAsync State:\n{ws.State}");
-            UnityEngine.Debug.Log("pre get tabs");
-            StartCoroutine(GetOpenTabs());
-            UnityEngine.Debug.Log("post  get tabs");
+            tab = bt;
+            StartCoroutine(tab.CreateScreenshot(1920, 1080, (screenshot) =>
+            {
+                rawImage.texture = screenshot;
+            }));
         }));
-        StartCoroutine(CreateScreenshot());
+
     }
 
     // Update is called once per frame
     void Update()
     {
-
-    }
-    private IEnumerator GetOpenTabs()
-    {
-        yield return DevToolsApiRequest(false, "/json/list", (response) =>
-        {
-            UnityEngine.Debug.Log($"Currently open tabs:\n{response}");
-        });
-    }
-
-    private IEnumerator OpenNewTab(System.Action<Task> callback)
-    {
-        yield return DevToolsApiRequest(true, $"/json/new?{targetUrl}", (response) =>
-        {
-            pageTarget = JsonConvert.DeserializeObject<PageTargetInfo>(response, serializerSettings);
-            UnityEngine.Debug.Log($"tab WebSocket: '{pageTarget.WebSocketDebuggerUrl}'");
-
-            // open remote devtools websocket connection
-            /**
-             * Note: For webgl compatibility you need to use the JavaScript plugin, see also https://docs.unity3d.com/Manual/webgl-networking.html#UsingWebSockets
-             */
-            var t = ws.ConnectAsync(new System.Uri(pageTarget.WebSocketDebuggerUrl), cancellationTokenSource.Token);
-            t.ContinueWith(callback);
-        });
-        //yield return new WaitUntil(() => t.IsCompleted);
-    }
-    private IEnumerator CreateScreenshot()
-    {
-        yield return new WaitUntil(() => ws.State == WebSocketState.Open);
-        var screenshotCommand = new CaptureScreenshotCommand();
-        screenshotCommand.Clip = new ChromeDevToolsProtocol.WebSocketProtocol.Types.Viewport();
-        screenshotCommand.Clip.Width = 620;
-        screenshotCommand.Clip.Height = 480;
-        //var sendTask = SendWsMessage(screenshotCommand);
-        var rect = this.GetComponent<RectTransform>().rect;
-        UnityEngine.Debug.Log($"pre, w: {rect.width} h: {rect.height}");
-        var sendTask = ws.SendAsync(Encoding.UTF8.GetBytes("{\"method\":\"Page.captureScreenshot\",\"id\":1,\"params\": {\"clip\":{\"x\":0,\"y\":0,\"width\":900,\"height\":560,\"scale\":1}}}"), WebSocketMessageType.Text, true, cancellationTokenSource.Token);
-        UnityEngine.Debug.Log("post");
-        yield return new WaitUntil(() => sendTask.IsCompleted);
-
-        var receiveTask = ReadWsMessage<CaptureScreenshotCommandResponse>();
-        yield return new WaitUntil(() => receiveTask.IsCompleted);
-
-        UnityEngine.Debug.Log($"screenshot result: '{receiveTask.Result["result"]["data"]}'");
-        //var screenshot = JsonConvert.DeserializeObject<CaptureScreenshotCommandResponse>(receiveTask.Result.Result, serializerSettings);
-        //UnityEngine.Debug.Log($"screenshot {receiveTask.Result.Id}: '{screenshot.Data}'");
-        byte[] imgBytes = Convert.FromBase64String(receiveTask.Result["result"]["data"].ToString());
-
-        UnityEngine.Debug.Log($"imgBytes.Length {imgBytes.Length}");
-        var myTexture = new Texture2D(1, 1);
-        myTexture.LoadImage(imgBytes);
-        //myTexture.Apply();
-        rawImage.texture = myTexture;
-
-        yield return StartCoroutine(CreateScreenshot());
-    }
-
-    /// <summary>
-    /// read and deserialize a json message from the devtools websocket
-    /// </summary>
-    /// <typeparam name="T">IDevtoolsCommandResponse</typeparam>
-    /// <returns></returns>
-    private async Task<JObject> ReadWsMessage<T>()
-    {
-        if (ws.State != WebSocketState.Open) throw new InvalidOperationException($"WebSocket is not open: ws.State = {ws.State}");
-
-        using (var ms = new MemoryStream())
-        {
-            WebSocketReceiveResult result;
-            do
-            {
-                var messageBuffer = WebSocket.CreateClientBuffer(1024, 16);
-                result = await ws.ReceiveAsync(messageBuffer, cancellationTokenSource.Token);
-                ms.Write(messageBuffer.Array, messageBuffer.Offset, result.Count);
-            }
-            while (!result.EndOfMessage);
-
-            if (result.MessageType == WebSocketMessageType.Text)
-            {
-                var msgString = Encoding.UTF8.GetString(ms.ToArray());
-                UnityEngine.Debug.Log($"ws received: {msgString}");
-                var response = JObject.Parse(msgString);
-                UnityEngine.Debug.Log($"ws received: {response["method"]}");
-                UnityEngine.Debug.Log($"ws received: {response["result"]}");
-                //return response.ToObject<DevtoolsCommandResponse<CaptureScreenshotCommandResponse>>(serializer);
-                return response;
-            }
-            else
-            {
-                throw new InvalidDataException($"Unexpected WebSocketMessageType: {result.MessageType}");
-            }
-            //ms.Seek(0, SeekOrigin.Begin);
-            //ms.Position = 0;
-        }
-    }
-
-    /// <summary>
-    /// send a json serializable message over the devtools websocket
-    /// </summary>
-    /// <typeparam name="T">IDevtoolsCommand</typeparam>
-    /// <param name="jsonSerializableMessage"></param>
-    /// <returns></returns>
-    private async Task SendWsMessage<T>(T jsonSerializableMessage)
-    {
-        if (ws.State != WebSocketState.Open) throw new InvalidOperationException($"WebSocket is not open: ws.State = {ws.State}");
-
-        var json = JsonConvert.SerializeObject(jsonSerializableMessage, serializerSettings);
-        UnityEngine.Debug.Log($"ws send: {json}");
-        await ws.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, cancellationTokenSource.Token);
-    }
-
-    /**
-     * Launch headless chrome browser with remote-debugging enabled, if not already running.
-     */
-    private static void LaunchBrowser()
-    {
-        // allow only one instance of chrome
-        if (BrowserView.browserProcess == null || BrowserView.browserProcess.HasExited)
-        {
-            BrowserView.browserProcess = new Process();
-            BrowserView.browserProcess.StartInfo.FileName = browserExecutablePath;
-            BrowserView.browserProcess.StartInfo.Arguments = $"--user-data-dir={Path.Join(Application.temporaryCachePath, "BrowserView")} --remote-debugging-port={debugPort} --remote-allow-origins=http://localhost:{debugPort}";
-
-            // set headlessBrowser to false to see the browser window
-            if (headlessBrowser)
-            {
-                BrowserView.browserProcess.StartInfo.Arguments = string.Concat(BrowserView.browserProcess.StartInfo.Arguments, "--headless");
-            }
-            else
-            {
-                BrowserView.browserProcess.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
-            }
-
-            BrowserView.browserProcess.Start();
-            UnityEngine.Debug.Log("launched chrome");
-        }
-    }
-
-    private IEnumerator DevToolsApiRequest(bool isPUT, string apiAddress, System.Action<string> callback)
-    {
-        UnityEngine.Debug.Log($"DevTools api Request: {apiAddress}");
-        UnityWebRequest webRequest;
-        if (isPUT)
-        {
-            webRequest = UnityWebRequest.Put($"http://localhost:{debugPort}{apiAddress}", "");
-        }
-        else
-        {
-            webRequest = UnityWebRequest.Get($"http://localhost:{debugPort}{apiAddress}");
-        }
-        yield return webRequest.SendWebRequest();
-
-        if (webRequest.result != UnityWebRequest.Result.Success)
-        {
-            UnityEngine.Debug.LogError(webRequest.error);
-            //TODO: handle error
-        }
-        else
-        {
-            UnityEngine.Debug.Log($"DevTools api response (for {apiAddress}):\n{webRequest.downloadHandler.text}");
-            callback(webRequest.downloadHandler.text);
-        }
+        
     }
 
     private void OnDisable()
     {
         // TODO: do we want to close the browser when not in use?
         // close browser when recompiling
-        if (browserProcess != null && !browserProcess.HasExited)
-        {
-            browserProcess.Kill();
-        }
-
-        UnityEngine.Debug.Log("BrowserView OnDestroy called");
-        ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "BrowerView closed", CancellationToken.None)
-            .ContinueWith(delegate
-            {
-                UnityEngine.Debug.Log($"ws CloseAsync State:\n{ws.State}");
-            });
-        // TODO: fix close tab
-        // -> Coroutine couldn't be started because the the game object 'BrowserView' is inactive!
-        // consider using OnEnable and OnDisable
-        // see also https://stackoverflow.com/a/67699419
-        StartCoroutine(DevToolsApiRequest(false, $"/json/close/{pageTarget.Id}", (response) =>
-        {
-            UnityEngine.Debug.Log("browser tab has been closed");
-        }));
+        tab.Close();
+        browser.Close();
     }
     private void OnDestroy()
     {
-        ws.Dispose();
-        cancellationTokenSource.Dispose();
+        tab.Close();
+        browser.Close();
     }
     /**
      * Close all browser windows.
      */
     void OnApplicationQuit()
     {
-        if (browserProcess != null && !browserProcess.HasExited)
-        {
-            browserProcess.Kill();
-        }
+        tab.Close();
+        browser.Close();
     }
 }
-
-/**
- * Json structs used in the chrome devtools protocol
- */
-namespace ChromeDevToolsProtocol
-{
-    // suppress style warning, because names must correspond to json keys
-    //[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Benennungsstile", Justification = "<Ausstehend>")]
-    public class PageTargetInfo
-    {
-        public string Description { get; set; }
-        public string DevtoolsFrontendUrl { get; set; }
-        public string Id { get; set; }
-        public string Title { get; set; }
-        public string Type { get; set; }
-        public string Url { get; set; }
-        public string WebSocketDebuggerUrl { get; set; }
-    }
-
-    namespace WebSocketProtocol
-    {
-        using Types;
-        public interface IDevtoolsCommand
-        {
-            long Id { get; }
-            string Method { get; }
-        }
-
-        ///
-        /// Every devtools command has an id and a method
-        ///
-        public class DevtoolsCommand : IDevtoolsCommand
-        {
-            private static long LAST_ID = 0;
-            public long Id { get; set; } = ++LAST_ID;
-            public string Method { get; set; }
-        }
-
-        ///
-        /// Every devtools command response has the same id and a method as the corresponding command
-        ///
-        public class DevtoolsCommandResponse<T>
-        {
-            public long Id { get; }
-            public string Method { get; }
-            public T Result { get; }
-        }
-
-        /// <summary>
-        /// Capture page screenshot.
-        /// </summary>
-        public class CaptureScreenshotCommand : DevtoolsCommand
-        {
-            public string Method { get; } = "Page.captureScreenshot";
-            /// <summary>
-            /// Gets or sets Image compression format (defaults to png).
-            /// </summary>
-            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
-            public string Format { get; set; }
-            /// <summary>
-            /// Gets or sets Compression quality from range [0..100] (jpeg only).
-            /// </summary>
-            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
-            public long? Quality { get; set; }
-            /// <summary>
-            /// Gets or sets Capture the screenshot of a given region only.
-            /// </summary>
-            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
-            public Viewport Clip { get; set; }
-            /// <summary>
-            /// Gets or sets Capture the screenshot from the surface, rather than the view. Defaults to true.
-            /// </summary>
-            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
-            public bool? FromSurface { get; set; }
-        }
-        /// <summary>
-        /// Capture page screenshot response.
-        /// </summary>
-        public class CaptureScreenshotCommandResponse
-        {
-            /// <summary>
-            /// Gets or sets Base64-encoded image data.
-            /// </summary>
-            public string Data { get; }
-        }
-
-        namespace Types
-        {
-            /// <summary>
-            /// Viewport for capturing screenshot.
-            /// </summary>
-            public class Viewport
-            {
-                /// <summary>
-                /// Gets or sets X offset in CSS pixels.
-                /// </summary>
-                public double X { get; set; } = 0;
-                /// <summary>
-                /// Gets or sets Y offset in CSS pixels
-                /// </summary>
-                public double Y { get; set; } = 0;
-                /// <summary>
-                /// Gets or sets Rectangle width in CSS pixels
-                /// </summary>
-                public double Width { get; set; } = 1920;
-                /// <summary>
-                /// Gets or sets Rectangle height in CSS pixels
-                /// </summary>
-                public double Height { get; set; } = 1080;
-                /// <summary>
-                /// Gets or sets Page scale factor.
-                /// </summary>
-                public double Scale { get; set; } = 1;
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools.meta b/Runtime/ChromeDevtools.meta
new file mode 100644
index 0000000..decabca
--- /dev/null
+++ b/Runtime/ChromeDevtools.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 55b01866b6fcd8543b11feed3428e70a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Browser.cs b/Runtime/ChromeDevtools/Browser.cs
new file mode 100644
index 0000000..2be57fc
--- /dev/null
+++ b/Runtime/ChromeDevtools/Browser.cs
@@ -0,0 +1,146 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Collections;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace ChromeDevTools
+{
+    public class Browser
+    {
+        /* singleton */
+        private static Browser instance;
+        private static Process browserProcess;
+
+        /* browser settings */
+        private const string browserExecutablePath = "chrome";
+        private const bool headlessBrowser = false;
+        private const int debugPort = 9222;
+
+        /* JsonSerializer */
+        public static JsonSerializer serializer;
+        public static JsonSerializerSettings serializerSettings;
+
+        public static CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
+
+        /**
+         * get singleton instance
+         */
+        public static Browser getInstance()
+        {
+            if (instance == null)
+            {
+                instance = new Browser();
+                instance.launchBrowser();
+            }
+            return instance;
+        }
+
+        /**
+         * constructor for the static fields
+         */
+        static Browser()
+        {
+            // initialize the JsonSerializer
+            var camelCasePropertyNamesContractResolver = new CamelCasePropertyNamesContractResolver();
+            serializer = new JsonSerializer();
+            serializer.ContractResolver = camelCasePropertyNamesContractResolver;
+            serializerSettings = new JsonSerializerSettings();
+            serializerSettings.ContractResolver = camelCasePropertyNamesContractResolver;
+        }
+
+        /**
+        * Launch headless chrome browser with remote-debugging enabled, if not already running.
+        */
+        private  void launchBrowser()
+        {
+            // allow only one instance of chrome
+            if (Browser.browserProcess == null || Browser.browserProcess.HasExited)
+            {
+                Browser.browserProcess = new Process();
+                Browser.browserProcess.StartInfo.FileName = browserExecutablePath;
+                Browser.browserProcess.StartInfo.Arguments = $"--user-data-dir={Path.Join(Application.temporaryCachePath, "BrowserView")} --remote-debugging-port={debugPort} --remote-allow-origins=http://localhost:{debugPort}";
+
+                // set headlessBrowser to false to see the browser window
+                if (headlessBrowser)
+                {
+                    Browser.browserProcess.StartInfo.Arguments = string.Concat(Browser.browserProcess.StartInfo.Arguments, "--headless");
+                }
+                else
+                {
+                    Browser.browserProcess.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
+                }
+
+                Browser.browserProcess.Start();
+                UnityEngine.Debug.Log("launched chrome");
+            }
+        }
+
+        /**
+         * send web request to the devTools API
+         */
+        private IEnumerator DevToolsApiRequest(bool isPUT, string apiAddress, System.Action<string> callback)
+        {
+            UnityEngine.Debug.Log($"DevTools api Request: {apiAddress}");
+            UnityWebRequest webRequest;
+            if (isPUT)
+            {
+                webRequest = UnityWebRequest.Put($"http://localhost:{debugPort}{apiAddress}", "");
+            }
+            else
+            {
+                webRequest = UnityWebRequest.Get($"http://localhost:{debugPort}{apiAddress}");
+            }
+            yield return webRequest.SendWebRequest();
+
+            if (webRequest.result != UnityWebRequest.Result.Success)
+            {
+                UnityEngine.Debug.LogError(webRequest.error);
+                //TODO: handle error
+            }
+            else
+            {
+                UnityEngine.Debug.Log($"DevTools api response (for {apiAddress}):\n{webRequest.downloadHandler.text}");
+                callback(webRequest.downloadHandler.text);
+            }
+        }
+
+        public IEnumerator OpenNewTab(string targetUrl, System.Action<BrowserTab> callback)
+        {
+            yield return DevToolsApiRequest(true, $"/json/new?{targetUrl}", (response) =>
+            {
+                PageTargetInfo pageTarget = JsonConvert.DeserializeObject<PageTargetInfo>(response, serializerSettings);
+                callback(new BrowserTab(pageTarget));
+
+            });
+        }
+
+        /**
+         * Not implemented.
+         */
+        [Obsolete("Not implemented.", true)]
+        private IEnumerator GetOpenTabs()
+        {
+            yield return DevToolsApiRequest(false, "/json/list", (response) =>
+            {
+                UnityEngine.Debug.Log($"Currently open tabs:\n{response}");
+            });
+        }
+
+        /**
+         * Close the browser
+         */
+        public void Close()
+        {
+            cancellationTokenSource.Cancel();
+            if (browserProcess != null && !browserProcess.HasExited)
+            {
+                browserProcess.Kill();
+            }
+        }
+    }
+}
diff --git a/Runtime/ChromeDevtools/Browser.cs.meta b/Runtime/ChromeDevtools/Browser.cs.meta
new file mode 100644
index 0000000..b329d89
--- /dev/null
+++ b/Runtime/ChromeDevtools/Browser.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7d88cbe2d5d249843862fd01330b5f19
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/BrowserTab.cs b/Runtime/ChromeDevtools/BrowserTab.cs
new file mode 100644
index 0000000..380dbb5
--- /dev/null
+++ b/Runtime/ChromeDevtools/BrowserTab.cs
@@ -0,0 +1,134 @@
+using ChromeDevTools.Protocol;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections;
+using System.IO;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using UnityEngine;
+
+namespace ChromeDevTools
+{
+    public class BrowserTab
+    {
+        private ClientWebSocket ws = new ClientWebSocket();
+        private PageTargetInfo pageTarget;
+
+        public BrowserTab(PageTargetInfo pageTarget)
+        {
+            this.pageTarget = pageTarget;
+            Debug.Log($"tab WebSocket: '{pageTarget.WebSocketDebuggerUrl}'");
+
+            // open remote devtools websocket connection
+            /**
+             * Note: Forebg wl compatibility you need to use the JavaScript plugin, see also https://docs.unity3d.com/Manual/webgl-networking.html#UsingWebSockets
+             */
+            var t = ws.ConnectAsync(new Uri(pageTarget.WebSocketDebuggerUrl), Browser.cancellationTokenSource.Token);
+        }
+
+
+        /// <summary>
+        /// send a json serializable message over the devtools websocket
+        /// </summary>
+        /// <typeparam name="T">IDevtoolsCommand</typeparam>
+        /// <param name="jsonSerializableMessage"></param>
+        /// <returns></returns>
+        private async Task SendWsMessage<T>(T jsonSerializableMessage)
+        {
+            if (ws.State != WebSocketState.Open) throw new InvalidOperationException($"WebSocket is not open: ws.State = {ws.State}");
+
+            var json = JsonConvert.SerializeObject(jsonSerializableMessage, Browser.serializerSettings);
+            UnityEngine.Debug.Log($"ws send: '{json}'");
+            //await ws.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, Browser.cancellationTokenSource.Token);
+        }
+
+        /// <summary>
+        /// read and deserialize a json message from the devtools websocket
+        /// </summary>
+        /// <typeparam name="T">IDevtoolsCommandResponse</typeparam>
+        /// <returns></returns>
+        private async Task<JObject> ReadWsMessage<T>()
+        {
+            if (ws.State != WebSocketState.Open) throw new InvalidOperationException($"WebSocket is not open: ws.State = {ws.State}");
+
+            using (var ms = new MemoryStream())
+            {
+                WebSocketReceiveResult result;
+                do
+                {
+                    var messageBuffer = WebSocket.CreateClientBuffer(1024, 16);
+                    result = await ws.ReceiveAsync(messageBuffer, Browser.cancellationTokenSource.Token);
+                    ms.Write(messageBuffer.Array, messageBuffer.Offset, result.Count);
+                }
+                while (!result.EndOfMessage);
+
+                if (result.MessageType == WebSocketMessageType.Text)
+                {
+                    var msgString = Encoding.UTF8.GetString(ms.ToArray());
+                    UnityEngine.Debug.Log($"ws received: {msgString}");
+                    var response = JObject.Parse(msgString);
+                    UnityEngine.Debug.Log($"ws received: {response["method"]}");
+                    UnityEngine.Debug.Log($"ws received: {response["result"]}");
+                    //return response.ToObject<DevtoolsCommandResponse<CaptureScreenshotCommandResponse>>(serializer);
+                    return response;
+                }
+                else
+                {
+                    throw new InvalidDataException($"Unexpected WebSocketMessageType: {result.MessageType}");
+                }
+                //ms.Seek(0, SeekOrigin.Begin);
+                //ms.Position = 0;
+            }
+        }
+
+        public IEnumerator CreateScreenshot(double width, double height, System.Action<Texture2D> callback)
+        {
+            yield return new WaitUntil(() => ws.State == WebSocketState.Open);
+            var screenshotCommand = new CaptureScreenshotCommand();
+            screenshotCommand.Clip = new Protocol.Types.Viewport();
+            screenshotCommand.Clip.Width = width;
+            screenshotCommand.Clip.Height = height;
+            //var sendTask = 
+            SendWsMessage(screenshotCommand);
+            var screenshotCmd = "{\"method\":\"Page.captureScreenshot\",\"id\":1,\"params\": {\"clip\":{\"x\":0,\"y\":0,\"width\":900,\"height\":560,\"scale\":1}}}";
+            UnityEngine.Debug.Log($"SendAsy: '{screenshotCmd}'");
+            var sendTask = ws.SendAsync(Encoding.UTF8.GetBytes(screenshotCmd), WebSocketMessageType.Text, true, Browser.cancellationTokenSource.Token);
+            yield return new WaitUntil(() => sendTask.IsCompleted);
+
+            var receiveTask = ReadWsMessage<CaptureScreenshotCommandResponse>();
+            yield return new WaitUntil(() => receiveTask.IsCompleted);
+
+            Debug.Log($"screenshot result: '{receiveTask.Result["result"]["data"]}'");
+            //var screenshot = JsonConvert.DeserializeObject<CaptureScreenshotCommandResponse>(receiveTask.Result.Result, serializerSettings);
+            //UnityEngine.Debug.Log($"screenshot {receiveTask.Result.Id}: '{screenshot.Data}'");
+            byte[] imgBytes = Convert.FromBase64String(receiveTask.Result["result"]["data"].ToString());
+
+            Debug.Log($"imgBytes.Length {imgBytes.Length}");
+            var myTexture = new Texture2D(1, 1);
+            myTexture.LoadImage(imgBytes);
+            //myTexture.Apply();
+            callback(myTexture);
+        }
+
+        /**
+         * close this tab
+         */
+        public void Close()
+        {
+            Debug.Log($"BrowserTab close called for: '{pageTarget.Url}'");
+            SendWsMessage(new CloseTargetCommand(pageTarget.Id))
+                .ContinueWith(delegate
+                {
+                    ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "tab closed", CancellationToken.None);
+                })
+                .ContinueWith(delegate
+                {
+                    Debug.Log($"ws CloseAsync State:\n{ws.State}");
+                    ws.Dispose();
+                });
+        }
+    }
+}
diff --git a/Runtime/ChromeDevtools/BrowserTab.cs.meta b/Runtime/ChromeDevtools/BrowserTab.cs.meta
new file mode 100644
index 0000000..1e43014
--- /dev/null
+++ b/Runtime/ChromeDevtools/BrowserTab.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b6518649de7ad344c9be14d7bf1a4db7
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/PageTargetInfo.cs b/Runtime/ChromeDevtools/PageTargetInfo.cs
new file mode 100644
index 0000000..70911ed
--- /dev/null
+++ b/Runtime/ChromeDevtools/PageTargetInfo.cs
@@ -0,0 +1,16 @@
+/**
+ * Json structs used in the chrome devtools protocol
+ */
+namespace ChromeDevTools
+{
+    public class PageTargetInfo
+    {
+        public string Description { get; set; }
+        public string DevtoolsFrontendUrl { get; set; }
+        public string Id { get; set; }
+        public string Title { get; set; }
+        public string Type { get; set; }
+        public string Url { get; set; }
+        public string WebSocketDebuggerUrl { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/PageTargetInfo.cs.meta b/Runtime/ChromeDevtools/PageTargetInfo.cs.meta
new file mode 100644
index 0000000..6564d2c
--- /dev/null
+++ b/Runtime/ChromeDevtools/PageTargetInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 89cc7fb90858ca64bb12fb93dc432385
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol.meta b/Runtime/ChromeDevtools/Protocol.meta
new file mode 100644
index 0000000..63d0001
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0dd7752c9faf69244aa677bdf6aa9d8e
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs b/Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs
new file mode 100644
index 0000000..88bcc2a
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs
@@ -0,0 +1,47 @@
+using Newtonsoft.Json;
+using ChromeDevTools.Protocol.Types;
+
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        /// <summary>
+        /// Capture page screenshot.
+        /// </summary>
+        public class CaptureScreenshotCommand : DevtoolsCommand
+        {
+            public string Method { get; } = "Page.captureScreenshot";
+            /// <summary>
+            /// Gets or sets Image compression format (defaults to png).
+            /// </summary>
+            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+            public string Format { get; set; }
+            /// <summary>
+            /// Gets or sets Compression quality from range [0..100] (jpeg only).
+            /// </summary>
+            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+            public long? Quality { get; set; }
+            /// <summary>
+            /// Gets or sets Capture the screenshot of a given region only.
+            /// </summary>
+            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+            public Viewport Clip { get; set; }
+            /// <summary>
+            /// Gets or sets Capture the screenshot from the surface, rather than the view. Defaults to true.
+            /// </summary>
+            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+            public bool? FromSurface { get; set; }
+        }
+
+        /// <summary>
+        /// Capture page screenshot response.
+        /// </summary>
+        public class CaptureScreenshotCommandResponse
+        {
+            /// <summary>
+            /// Gets or sets Base64-encoded image data.
+            /// </summary>
+            public string Data { get; }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs.meta b/Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs.meta
new file mode 100644
index 0000000..8fb5017
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/CaptureScreenshot.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bc86ea115d2a9514f838cecf8f84ccd0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs b/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs
new file mode 100644
index 0000000..f1dbd18
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs
@@ -0,0 +1,25 @@
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        ///
+        /// Every devtools command has an id and a method
+        ///
+        public class DevtoolsCommand
+        {
+            private static long LAST_ID = 0;
+            public long Id { get; } = ++LAST_ID;
+            public string Method { get; set; }
+        }
+
+        ///
+        /// Every devtools command response has the same id and a method as the corresponding command
+        ///
+        public class DevtoolsCommandResponse<T>
+        {
+            public long Id { get; }
+            public string Method { get; }
+            public T Result { get; }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs.meta b/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs.meta
new file mode 100644
index 0000000..ac9d087
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7a3faba1943f2cb46bb7219bf7893256
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/Target.meta b/Runtime/ChromeDevtools/Protocol/Target.meta
new file mode 100644
index 0000000..4a6a7de
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Target.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9cc5d9c0c13f4904fa6b855e8a1f5f37
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs b/Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs
new file mode 100644
index 0000000..79fb6a7
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs
@@ -0,0 +1,34 @@
+using Newtonsoft.Json;
+using ChromeDevTools.Protocol.Types;
+
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        /// <summary>
+        /// Closes the target. If the target is a page that gets closed too.
+        /// </summary>
+        public class CloseTargetCommand : DevtoolsCommand
+        {
+            public string Method { get; } = "Target.closeTarget";
+
+            public CloseTargetCommand(string targetId) => TargetId = targetId;
+
+            /// <summary>
+            /// Gets or sets TargetId
+            /// </summary>
+            public string TargetId { get; set; }
+        }
+
+        /// <summary>
+        /// Closes the target. If the target is a page that gets closed too.
+        /// </summary>
+        public class CloseTargetCommandResponse
+        {
+            /// <summary>
+            /// Always set to true. If an error occurs, the response indicates protocol error.
+            /// </summary>
+            public bool Success { get; }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs.meta b/Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs.meta
new file mode 100644
index 0000000..3120eaf
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Target/CloseTarget.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c7dcb963b668c7b439ba6dd59dda9d53
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/Types.meta b/Runtime/ChromeDevtools/Protocol/Types.meta
new file mode 100644
index 0000000..5b13830
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Types.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c939ea5aca9b41845bde1c648ba04625
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/Types/Viewport.cs b/Runtime/ChromeDevtools/Protocol/Types/Viewport.cs
new file mode 100644
index 0000000..474f043
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Types/Viewport.cs
@@ -0,0 +1,35 @@
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        namespace Types
+        {
+            /// <summary>
+            /// Viewport for capturing screenshot.
+            /// </summary>
+            public class Viewport
+            {
+                /// <summary>
+                /// Gets or sets X offset in CSS pixels.
+                /// </summary>
+                public double X { get; set; } = 0;
+                /// <summary>
+                /// Gets or sets Y offset in CSS pixels
+                /// </summary>
+                public double Y { get; set; } = 0;
+                /// <summary>
+                /// Gets or sets Rectangle width in CSS pixels
+                /// </summary>
+                public double Width { get; set; } = 1920;
+                /// <summary>
+                /// Gets or sets Rectangle height in CSS pixels
+                /// </summary>
+                public double Height { get; set; } = 1080;
+                /// <summary>
+                /// Gets or sets Page scale factor.
+                /// </summary>
+                public double Scale { get; set; } = 1;
+            }
+        }
+    }
+}
diff --git a/Runtime/ChromeDevtools/Protocol/Types/Viewport.cs.meta b/Runtime/ChromeDevtools/Protocol/Types/Viewport.cs.meta
new file mode 100644
index 0000000..079ef8d
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Types/Viewport.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 19142ebe654b31e46b2b8b740d5093e1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
-- 
GitLab