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