From ed530a89fda8c1212e85046460127f8f0ae493fd Mon Sep 17 00:00:00 2001
From: Bjoern Esswein <bjoern.esswein@gmail.com>
Date: Fri, 6 Oct 2023 22:13:47 +0200
Subject: [PATCH] WIP: added devtools screencast and devtools events

TODO: implement receiving events
---
 BrowserView.prefab                            |  12 +-
 Runtime/BrowserView.cs                        |  15 +-
 Runtime/ChromeDevtools/Browser.cs             |   8 +-
 Runtime/ChromeDevtools/Protocol/Cast.meta     |   8 +
 Runtime/ChromeDevtools/Protocol/Cast/Cast.cs  | 124 ++++++++++++++++
 .../ChromeDevtools/Protocol/Cast/Cast.cs.meta |  11 ++
 .../Protocol/DevtoolsCommand.cs               |  15 --
 .../ChromeDevtools/Protocol/DevtoolsEvent.cs  |  23 +++
 .../Protocol/DevtoolsEvent.cs.meta            |  11 ++
 .../Protocol/DevtoolsResponse.cs              |  23 +++
 .../Protocol/DevtoolsResponse.cs.meta         |  11 ++
 .../Protocol/Page/Screencast.cs               | 137 ++++++++++++++++++
 .../Protocol/Page/Screencast.cs.meta          |  11 ++
 13 files changed, 377 insertions(+), 32 deletions(-)
 create mode 100644 Runtime/ChromeDevtools/Protocol/Cast.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/Cast/Cast.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/Cast/Cast.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/Page/Screencast.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/Page/Screencast.cs.meta

diff --git a/BrowserView.prefab b/BrowserView.prefab
index dbdb259..732b376 100644
--- a/BrowserView.prefab
+++ b/BrowserView.prefab
@@ -10,8 +10,8 @@ GameObject:
   m_Component:
   - component: {fileID: 2974656142881083530}
   - component: {fileID: 5559415116192402672}
-  - component: {fileID: 4531839590213585742}
-  - component: {fileID: 5511584970398147175}
+  - component: {fileID: 3767983593885104582}
+  - component: {fileID: 2459857651630437765}
   m_Layer: 5
   m_Name: BrowserView
   m_TagString: Untagged
@@ -47,7 +47,7 @@ CanvasRenderer:
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 8825460134736404412}
   m_CullTransparentMesh: 1
---- !u!114 &4531839590213585742
+--- !u!114 &3767983593885104582
 MonoBehaviour:
   m_ObjectHideFlags: 0
   m_CorrespondingSourceObject: {fileID: 0}
@@ -67,14 +67,14 @@ MonoBehaviour:
   m_OnCullStateChanged:
     m_PersistentCalls:
       m_Calls: []
-  m_Texture: {fileID: 0}
+  m_Texture: {fileID: 2800000, guid: 7ee48c5fba4919649a8a6094cbead669, type: 3}
   m_UVRect:
     serializedVersion: 2
     x: 0
     y: 0
     width: 1
     height: 1
---- !u!114 &5511584970398147175
+--- !u!114 &2459857651630437765
 MonoBehaviour:
   m_ObjectHideFlags: 0
   m_CorrespondingSourceObject: {fileID: 0}
@@ -86,4 +86,4 @@ MonoBehaviour:
   m_Script: {fileID: 11500000, guid: 6893fa42f929f894286c6f00c40bcd06, type: 3}
   m_Name: 
   m_EditorClassIdentifier: 
-  targetUrl: https://google.de
+  headlessBrowser: 1
diff --git a/Runtime/BrowserView.cs b/Runtime/BrowserView.cs
index 86ad1fb..32bc7be 100644
--- a/Runtime/BrowserView.cs
+++ b/Runtime/BrowserView.cs
@@ -1,28 +1,29 @@
 using ChromeDevTools;
 using System.Collections;
-using System.Collections.Generic;
 using UnityEngine;
 using UnityEngine.UI;
-//using Unity.Networking.Transport;
 
 [RequireComponent(typeof(RawImage))]
-public class BrowserView : MonoBehaviour //TODO: Extends RawImage instead?
+public class BrowserView : MonoBehaviour
 {
     private Browser browser;
     private BrowserTab tab;
 
     private RawImage rawImage;
 
+    public bool headlessBrowser = true;
+    //TODO: handle changed targetUrl
     public string targetUrl = "https://google.de";
 
     // Start is called before the first frame update
-    void Start()
+    private void Start()
     {
         rawImage = this.gameObject.GetComponent<RawImage>();
     }
 
-    void OnEnable()
+    private void OnEnable()
     {
+        Browser.headless = headlessBrowser;
         browser = Browser.getInstance();
 
         //StartCoroutine(GetOpenTabs());
@@ -45,7 +46,7 @@ public class BrowserView : MonoBehaviour //TODO: Extends RawImage instead?
     }
 
     // Update is called once per frame
-    void Update()
+    private void Update()
     {
         
     }
@@ -65,7 +66,7 @@ public class BrowserView : MonoBehaviour //TODO: Extends RawImage instead?
     /**
      * Close all browser windows.
      */
-    void OnApplicationQuit()
+    private void OnApplicationQuit()
     {
         tab.Close();
         browser.Close();
diff --git a/Runtime/ChromeDevtools/Browser.cs b/Runtime/ChromeDevtools/Browser.cs
index 49fad21..b57c646 100644
--- a/Runtime/ChromeDevtools/Browser.cs
+++ b/Runtime/ChromeDevtools/Browser.cs
@@ -17,8 +17,8 @@ namespace ChromeDevTools
         private static Process browserProcess;
 
         /* browser settings */
-        private const string browserExecutablePath = "chrome";
-        private const bool headlessBrowser = false;
+        public static string browserExecutablePath = "chrome";
+        public static bool headless = true;
         private const int debugPort = 9222;
 
         /* JsonSerializer */
@@ -63,10 +63,10 @@ namespace ChromeDevTools
             {
                 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}";
+                Browser.browserProcess.StartInfo.Arguments = $"--user-data-dir={Path.Join(Application.temporaryCachePath, "BrowserView")} --remote-debugging-port={debugPort} --remote-allow-origins=http://localhost:{debugPort} --hide-crash-restore-bubble";
 
                 // set headlessBrowser to false to see the browser window
-                if (headlessBrowser)
+                if (headless)
                 {
                     Browser.browserProcess.StartInfo.Arguments = string.Concat(Browser.browserProcess.StartInfo.Arguments, " --headless=new");
                 }
diff --git a/Runtime/ChromeDevtools/Protocol/Cast.meta b/Runtime/ChromeDevtools/Protocol/Cast.meta
new file mode 100644
index 0000000..f9da9a2
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Cast.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1f4e0afa13be5aa4da5b06d9d861002a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/Cast/Cast.cs b/Runtime/ChromeDevtools/Protocol/Cast/Cast.cs
new file mode 100644
index 0000000..4a67cb3
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Cast/Cast.cs
@@ -0,0 +1,124 @@
+using Newtonsoft.Json;
+
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        namespace Cast
+        {
+        }
+    }
+}
+
+
+/*
+Devtools protocol.json:
+{
+    "domain": "Cast",
+    "description": "A domain for interacting with Cast, Presentation API, and Remote Playback API\nfunctionalities.",
+    "experimental": true,
+    "types": [
+        {
+            "id": "Sink",
+            "type": "object",
+            "properties": [
+                {
+                    "name": "name",
+                    "type": "string"
+                },
+                {
+                    "name": "id",
+                    "type": "string"
+                },
+                {
+                    "name": "session",
+                    "description": "Text describing the current session. Present only if there is an active\nsession on the sink.",
+                    "optional": true,
+                    "type": "string"
+                }
+            ]
+        }
+    ],
+    "commands": [
+        {
+            "name": "enable",
+            "description": "Starts observing for sinks that can be used for tab mirroring, and if set,\nsinks compatible with |presentationUrl| as well. When sinks are found, a\n|sinksUpdated| event is fired.\nAlso starts observing for issue messages. When an issue is added or removed,\nan |issueUpdated| event is fired.",
+            "parameters": [
+                {
+                    "name": "presentationUrl",
+                    "optional": true,
+                    "type": "string"
+                }
+            ]
+        },
+        {
+            "name": "disable",
+            "description": "Stops observing for sinks and issues."
+        },
+        {
+            "name": "setSinkToUse",
+            "description": "Sets a sink to be used when the web page requests the browser to choose a\nsink via Presentation API, Remote Playback API, or Cast SDK.",
+            "parameters": [
+                {
+                    "name": "sinkName",
+                    "type": "string"
+                }
+            ]
+        },
+        {
+            "name": "startDesktopMirroring",
+            "description": "Starts mirroring the desktop to the sink.",
+            "parameters": [
+                {
+                    "name": "sinkName",
+                    "type": "string"
+                }
+            ]
+        },
+        {
+            "name": "startTabMirroring",
+            "description": "Starts mirroring the tab to the sink.",
+            "parameters": [
+                {
+                    "name": "sinkName",
+                    "type": "string"
+                }
+            ]
+        },
+        {
+            "name": "stopCasting",
+            "description": "Stops the active Cast session on the sink.",
+            "parameters": [
+                {
+                    "name": "sinkName",
+                    "type": "string"
+                }
+            ]
+        }
+    ],
+    "events": [
+        {
+            "name": "sinksUpdated",
+            "description": "This is fired whenever the list of available sinks changes. A sink is a\ndevice or a software surface that you can cast to.",
+            "parameters": [
+                {
+                    "name": "sinks",
+                    "type": "array",
+                    "items": {
+                        "$ref": "Sink"
+                    }
+                }
+            ]
+        },
+        {
+            "name": "issueUpdated",
+            "description": "This is fired whenever the outstanding issue/error message changes.\n|issueMessage| is empty if there is no issue.",
+            "parameters": [
+                {
+                    "name": "issueMessage",
+                    "type": "string"
+                }
+            ]
+        }
+    ]
+}*/
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/Cast/Cast.cs.meta b/Runtime/ChromeDevtools/Protocol/Cast/Cast.cs.meta
new file mode 100644
index 0000000..d2b9e03
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Cast/Cast.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b4f546ce82c7cb84b8003bc93d5cf73f
+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
index 0fd27d7..1601a85 100644
--- a/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs
+++ b/Runtime/ChromeDevtools/Protocol/DevtoolsCommand.cs
@@ -25,20 +25,5 @@ namespace ChromeDevTools
             public DevtoolsCommandWrapper(T command) => Params = command;
         }
         public interface IDevtoolsCommand {}
-
-        public class DevtoolsResponseWrapper
-        {
-            public long Id { get; set; }
-            public JObject Result { get; set; }
-        }
-        ///
-        /// Every devtools command response has the same id and a method as the corresponding command
-        ///
-        public class DevtoolsResponseWrapper<T>: DevtoolsResponseWrapper where T: IDevtoolsResponse
-        {
-            public new T Result { get; set; }
-        }
-
-        public interface IDevtoolsResponse {}
     }
 }
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs b/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs
new file mode 100644
index 0000000..a4c124e
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs
@@ -0,0 +1,23 @@
+using Newtonsoft.Json.Linq;
+using System;
+
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        public class DevtoolsEventWrapper
+        {
+            public string Method { get; set; }
+            public JObject Params { get; set; }
+        }
+        ///
+        /// Every devtools command response has the same id and a method as the corresponding command
+        ///
+        public class DevtoolsEventWrapper<T> : DevtoolsEventWrapper where T : IDevtoolsEvent
+        {
+            public new T Params { get; set; }
+        }
+
+        public interface IDevtoolsEvent { }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs.meta b/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs.meta
new file mode 100644
index 0000000..e4ed691
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 399f2cceede40aa4ebe787a7f276de78
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs b/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs
new file mode 100644
index 0000000..8d4f55c
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs
@@ -0,0 +1,23 @@
+using Newtonsoft.Json.Linq;
+using System;
+
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        public class DevtoolsResponseWrapper
+        {
+            public long Id { get; set; }
+            public JObject Result { get; set; }
+        }
+        ///
+        /// Every devtools command response has the same id and a method as the corresponding command
+        ///
+        public class DevtoolsResponseWrapper<T> : DevtoolsResponseWrapper where T : IDevtoolsResponse
+        {
+            public new T Result { get; set; }
+        }
+
+        public interface IDevtoolsResponse { }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs.meta b/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs.meta
new file mode 100644
index 0000000..ea0de6b
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 81e639e1e8ae9084ab868731d8c5742f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/Page/Screencast.cs b/Runtime/ChromeDevtools/Protocol/Page/Screencast.cs
new file mode 100644
index 0000000..d3dac2e
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Page/Screencast.cs
@@ -0,0 +1,137 @@
+using Newtonsoft.Json;
+
+namespace ChromeDevTools
+{
+    namespace Protocol
+    {
+        namespace Page
+        {
+            /// <summary>
+            /// Starts sending each frame using the `screencastFrame` event.
+            /// "experimental": true
+            /// 
+            /// No Response
+            /// </summary>
+            //[CommandResponseAttribute(typeof(StartScreencastCommandResponse))]
+            public class startScreencast : IDevtoolsCommand
+            {
+                /// <summary>
+                /// Gets or sets Image compression format (defaults to png).
+                /// Can be "jpeg" or "png"
+                /// </summary>
+                [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+                public string Format { get; set; }
+                /// <summary>
+                /// Gets or sets Compression quality from range [0..100].
+                /// </summary>
+                [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+                public int? Quality { get; set; }
+                /// <summary>
+                /// Maximum screenshot width.
+                /// </summary>
+                [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+                public int MaxWidth { get; set; }
+                /// <summary>
+                /// Maximum screenshot height.
+                /// </summary>
+                [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+                public int maxHeight { get; set; }
+                /// <summary>
+                /// Send every n-th frame.
+                /// </summary>
+                [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+                public int everyNthFrame { get; set; }
+            }
+
+
+            /// <summary>
+            /// Acknowledges that a screencast frame has been received by the frontend.
+            /// "experimental": true
+            /// </summary>
+            public class screencastFrameAck : IDevtoolsCommand
+            {
+                /// <summary>
+                /// Frame number.
+                /// </summary>
+                public int sessionId { get; set; }
+            }
+
+            /// <summary>
+            /// Stops sending each frame in the `screencastFrame`.
+            /// "experimental": true
+            /// </summary>
+            public class stopScreencast : IDevtoolsCommand
+            { }
+
+            /// <summary>
+            /// Compressed image data requested by the `startScreencast`.
+            /// </summary>
+            public class screencastFrameEvent : IDevtoolsEvent
+            {
+                /// <summary>
+                /// Gets or sets Base64-encoded image data.
+                /// </summary>
+                public string Data { get; set; }
+                /// <summary>
+                /// Screencast frame metadata.
+                /// </summary>
+                public Types.ScreencastFrameMetadata metadata { get; set; }
+                /// <summary>
+                /// Frame number.
+                /// </summary>
+                public int sessionId { get; set; }
+            }
+
+            /// <summary>
+            /// Fired when the page with currently enabled screencast was shown or hidden `.
+            /// </summary>
+            public class screencastVisibilityChanged : IDevtoolsEvent
+            {
+                /// <summary>
+                /// True if the page is visible.
+                /// </summary>
+                public bool visible { get; set; }
+            }
+
+            namespace Types
+            {
+                /// <summary>
+                /// Screencast frame metadata.
+                /// </summary>
+                public class ScreencastFrameMetadata
+                {
+                    /// <summary>
+                    /// Top offset in DIP.
+                    /// </summary>
+                    public int offsetTop { get; set; }
+                    /// <summary>
+                    /// Page scale factor.
+                    /// </summary>
+                    public int pageScaleFactor { get; set; }
+                    /// <summary>
+                    /// Device screen width in DIP.
+                    /// </summary>
+                    public int deviceWidth { get; set; }
+                    /// <summary>
+                    /// Device screen height in DIP.
+                    /// </summary>
+                    public int deviceHeight { get; set; }
+                    /// <summary>
+                    /// Position of horizontal scroll in CSS pixels.
+                    /// </summary>
+                    public int scrollOffsetX { get; set; }
+                    /// <summary>
+                    /// Position of vertical scroll in CSS pixels.
+                    /// </summary>
+                    public int scrollOffsetY { get; set; }
+                    /// <summary>
+                    /// Frame swap timestamp.
+                    /// UTC time in seconds, counted from January 1, 1970.
+                    /// </summary>
+                    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+                    public long? timestamp { get; set; }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/Page/Screencast.cs.meta b/Runtime/ChromeDevtools/Protocol/Page/Screencast.cs.meta
new file mode 100644
index 0000000..157140b
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/Page/Screencast.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7f7a2406fccd8c74d89d419e99a8bf29
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
-- 
GitLab