/**
* Copyright (c) 2021 Vuplex Inc. All rights reserved.
*
* Licensed under the Vuplex Commercial Software Library License, you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* https://vuplex.com/commercial-library-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Only define BaseWebView.cs on supported platforms to avoid IL2CPP linking
// errors on unsupported platforms.
#if UNITY_EDITOR || UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_ANDROID || (UNITY_IOS && !VUPLEX_OMIT_IOS) || UNITY_WSA ||UNITY_WEBGL
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using UnityEngine;
#if NET_4_6 || NET_STANDARD_2_0
using System.Threading.Tasks;
#endif
namespace Vuplex.WebView {
///
/// The base `IWebView` implementation, which is extended for each platform.
///
public abstract class BaseWebView : MonoBehaviour {
public event EventHandler CloseRequested;
public event EventHandler ConsoleMessageLogged {
add {
_consoleMessageLogged += value;
if (_consoleMessageLogged.GetInvocationList().Length == 1) {
_setConsoleMessageEventsEnabled(true);
}
}
remove {
_consoleMessageLogged -= value;
if (_consoleMessageLogged.GetInvocationList().Length == 0) {
_setConsoleMessageEventsEnabled(false);
}
}
}
public event EventHandler FocusedInputFieldChanged {
add {
_focusedInputFieldChanged += value;
if (_focusedInputFieldChanged.GetInvocationList().Length == 1) {
_setFocusedInputFieldEventsEnabled(true);
}
}
remove {
_focusedInputFieldChanged -= value;
if (_focusedInputFieldChanged.GetInvocationList().Length == 0) {
_setFocusedInputFieldEventsEnabled(false);
}
}
}
public event EventHandler LoadProgressChanged;
public event EventHandler> MessageEmitted;
public event EventHandler PageLoadFailed;
public event EventHandler> TitleChanged;
public event EventHandler UrlChanged;
public event EventHandler> VideoRectChanged;
public bool IsDisposed { get; protected set; }
public bool IsInitialized { get; private set; }
public List PageLoadScripts {
get {
return _pageLoadScripts;
}
}
public float Resolution {
get {
return _numberOfPixelsPerUnityUnit;
}
}
public Vector2 Size {
get {
return new Vector2(_width, _height);
}
}
public virtual Vector2 SizeInPixels {
get {
return new Vector2(_nativeWidth, _nativeHeight);
}
}
public Texture2D Texture {
get {
return _viewportTexture;
}
}
public string Url { get; private set; }
public Texture2D VideoTexture {
get {
return _videoTexture;
}
}
public void Init(Texture2D texture, float width, float height) {
Init(texture, width, height, null);
}
public virtual void Init(Texture2D viewportTexture, float width, float height, Texture2D videoTexture) {
if (IsInitialized) {
throw new InvalidOperationException("Init() cannot be called on a webview that has already been initialized.");
}
_viewportTexture = viewportTexture;
_videoTexture = videoTexture;
// Assign the game object a unique name so that the native view can send it messages.
gameObject.name = String.Format("WebView-{0}", Guid.NewGuid().ToString());
_width = width;
_height = height;
Utils.ThrowExceptionIfAbnormallyLarge(_nativeWidth, _nativeHeight);
IsInitialized = true;
// Prevent the script from automatically being destroyed when a new scene is loaded.
DontDestroyOnLoad(gameObject);
}
public virtual void Blur() {
_assertValidState();
WebView_blur(_nativeWebViewPtr);
}
#if NET_4_6 || NET_STANDARD_2_0
public Task CanGoBack() {
var task = new TaskCompletionSource();
CanGoBack(task.SetResult);
return task.Task;
}
public Task CanGoForward() {
var task = new TaskCompletionSource();
CanGoForward(task.SetResult);
return task.Task;
}
#endif
public virtual void CanGoBack(Action callback) {
_assertValidState();
_pendingCanGoBackCallbacks.Add(callback);
WebView_canGoBack(_nativeWebViewPtr);
}
public virtual void CanGoForward(Action callback) {
_assertValidState();
_pendingCanGoForwardCallbacks.Add(callback);
WebView_canGoForward(_nativeWebViewPtr);
}
#if NET_4_6 || NET_STANDARD_2_0
public Task CaptureScreenshot() {
var task = new TaskCompletionSource();
CaptureScreenshot(task.SetResult);
return task.Task;
}
#endif
public virtual void CaptureScreenshot(Action callback) {
var bytes = new byte[0];
try {
var texture = _getReadableTexture();
bytes = ImageConversion.EncodeToPNG(texture);
Destroy(texture);
} catch (Exception e) {
WebViewLogger.LogError("An exception occurred while capturing the screenshot: " + e);
}
callback(bytes);
}
public virtual void Click(Vector2 point) {
_assertValidState();
int nativeX = (int) (point.x * _nativeWidth);
int nativeY = (int) (point.y * _nativeHeight);
WebView_click(_nativeWebViewPtr, nativeX, nativeY);
}
public virtual void Click(Vector2 point, bool preventStealingFocus) {
// On most platforms, the regular `Click()` method
// doesn't steal focus.
Click(point);
}
public static void ClearAllData() {
WebView_clearAllData();
}
public static void CreateTexture(float width, float height, Action callback) {
int nativeWidth = (int)(width * Config.NumberOfPixelsPerUnityUnit);
int nativeHeight = (int)(height * Config.NumberOfPixelsPerUnityUnit);
Utils.ThrowExceptionIfAbnormallyLarge(nativeWidth, nativeHeight);
var texture = new Texture2D(
nativeWidth,
nativeHeight,
TextureFormat.RGBA32,
false,
false
);
#if UNITY_2020_2_OR_NEWER
// In Unity 2020.2, Unity's internal TexturesD3D11.cpp class on Windows logs an error if
// UpdateExternalTexture() is called on a Texture2D created from the constructor
// rather than from Texture2D.CreateExternalTexture(). So, rather than returning
// the original Texture2D created via the constructor, we return a copy created
// via CreateExternalTexture(). This approach is only used for 2020.2 and newer because
// it doesn't work in 2018.4 and instead causes a crash.
texture = Texture2D.CreateExternalTexture(
nativeWidth,
nativeHeight,
TextureFormat.RGBA32,
false,
false,
texture.GetNativeTexturePtr()
);
#endif
// Invoke the callback asynchronously in order to match the async
// behavior that's required for Android.
Dispatcher.RunOnMainThread(() => callback(texture));
}
public virtual void Copy() {
_assertValidState();
_getSelectedText(text => GUIUtility.systemCopyBuffer = text);
}
public virtual void Cut() {
_assertValidState();
_getSelectedText(text => {
GUIUtility.systemCopyBuffer = text;
HandleKeyboardInput("Backspace");
});
}
public virtual void DisableViewUpdates() {
_assertValidState();
WebView_disableViewUpdates(_nativeWebViewPtr);
_viewUpdatesAreEnabled = false;
}
public virtual void Dispose() {
_assertValidState();
IsDisposed = true;
WebView_destroy(_nativeWebViewPtr);
_nativeWebViewPtr = IntPtr.Zero;
// To avoid a MissingReferenceException, verify that this script
// hasn't already been destroyed prior to accessing gameObject.
if (this != null) {
Destroy(gameObject);
}
}
public virtual void EnableViewUpdates() {
_assertValidState();
if (_currentViewportNativeTexture != IntPtr.Zero) {
_viewportTexture.UpdateExternalTexture(_currentViewportNativeTexture);
}
WebView_enableViewUpdates(_nativeWebViewPtr);
_viewUpdatesAreEnabled = true;
}
#if NET_4_6 || NET_STANDARD_2_0
public Task ExecuteJavaScript(string javaScript) {
var task = new TaskCompletionSource();
ExecuteJavaScript(javaScript, task.SetResult);
return task.Task;
}
#else
public void ExecuteJavaScript(string javaScript) {
ExecuteJavaScript(javaScript, null);
}
#endif
public virtual void ExecuteJavaScript(string javaScript, Action callback) {
_assertValidState();
string resultCallbackId = null;
if (callback != null) {
resultCallbackId = Guid.NewGuid().ToString();
_pendingJavaScriptResultCallbacks[resultCallbackId] = callback;
}
WebView_executeJavaScript(_nativeWebViewPtr, javaScript, resultCallbackId);
}
public virtual void Focus() {
_assertValidState();
WebView_focus(_nativeWebViewPtr);
}
#if NET_4_6 || NET_STANDARD_2_0
public Task GetRawTextureData() {
var task = new TaskCompletionSource();
GetRawTextureData(task.SetResult);
return task.Task;
}
#endif
public virtual void GetRawTextureData(Action callback) {
var bytes = new byte[0];
try {
var texture = _getReadableTexture();
bytes = texture.GetRawTextureData();
Destroy(texture);
} catch (Exception e) {
WebViewLogger.LogError("An exception occurred while getting the raw texture data: " + e);
}
callback(bytes);
}
public static void GloballySetUserAgent(bool mobile) {
var success = WebView_globallySetUserAgentToMobile(mobile);
if (!success) {
throw new InvalidOperationException(USER_AGENT_EXCEPTION_MESSAGE);
}
}
public static void GloballySetUserAgent(string userAgent) {
var success = WebView_globallySetUserAgent(userAgent);
if (!success) {
throw new InvalidOperationException(USER_AGENT_EXCEPTION_MESSAGE);
}
}
public virtual void GoBack() {
_assertValidState();
WebView_goBack(_nativeWebViewPtr);
}
public virtual void GoForward() {
_assertValidState();
WebView_goForward(_nativeWebViewPtr);
}
public virtual void HandleKeyboardInput(string input) {
_assertValidState();
WebView_handleKeyboardInput(_nativeWebViewPtr, input);
}
public virtual void LoadHtml(string html) {
_assertValidState();
WebView_loadHtml(_nativeWebViewPtr, html);
}
public virtual void LoadUrl(string url) {
_assertValidState();
WebView_loadUrl(_nativeWebViewPtr, _transformStreamingAssetsUrlIfNeeded(url));
}
public virtual void LoadUrl(string url, Dictionary additionalHttpHeaders) {
_assertValidState();
if (additionalHttpHeaders == null) {
LoadUrl(url);
} else {
var headerStrings = additionalHttpHeaders.Keys.Select(key => String.Format("{0}: {1}", key, additionalHttpHeaders[key])).ToArray();
var newlineDelimitedHttpHeaders = String.Join("\n", headerStrings);
WebView_loadUrlWithHeaders(_nativeWebViewPtr, url, newlineDelimitedHttpHeaders);
}
}
public virtual void Paste() {
_assertValidState();
var text = GUIUtility.systemCopyBuffer;
foreach (var character in text) {
HandleKeyboardInput(char.ToString(character));
}
}
public void PostMessage(string data) {
var escapedString = data.Replace("'", "\\'").Replace("\n", "\\n");
ExecuteJavaScript(String.Format("vuplex._emit('message', {{ data: \'{0}\' }})", escapedString));
}
public virtual void Reload() {
_assertValidState();
WebView_reload(_nativeWebViewPtr);
}
public virtual void Resize(float width, float height) {
if (IsDisposed || (width == _width && height == _height)) {
return;
}
_width = width;
_height = height;
_resize();
}
public virtual void Scroll(Vector2 delta) {
_assertValidState();
var deltaX = (int)(delta.x * _numberOfPixelsPerUnityUnit);
var deltaY = (int)(delta.y * _numberOfPixelsPerUnityUnit);
WebView_scroll(_nativeWebViewPtr, deltaX, deltaY);
}
public virtual void Scroll(Vector2 scrollDelta, Vector2 point) {
_assertValidState();
var deltaX = (int)(scrollDelta.x * _numberOfPixelsPerUnityUnit);
var deltaY = (int)(scrollDelta.y * _numberOfPixelsPerUnityUnit);
var pointerX = (int)(point.x * _nativeWidth);
var pointerY = (int)(point.y * _nativeHeight);
WebView_scrollAtPoint(_nativeWebViewPtr, deltaX, deltaY, pointerX, pointerY);
}
public virtual void SelectAll() {
_assertValidState();
// If the focused element is an input with a select() method, then use that.
// Otherwise, travel up the DOM until we get to the body or a contenteditable
// element, and then select its contents.
ExecuteJavaScript(
@"(function() {
var element = document.activeElement || document.body;
while (!(element === document.body || element.getAttribute('contenteditable') === 'true')) {
if (typeof element.select === 'function') {
element.select();
return;
}
element = element.parentElement;
}
var range = document.createRange();
range.selectNodeContents(element);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
})();",
null
);
}
public void SetResolution(float pixelsPerUnityUnit) {
// Note: this method can be called prior to initialization.
_numberOfPixelsPerUnityUnit = pixelsPerUnityUnit;
_resize();
}
public static void SetStorageEnabled(bool enabled) {
WebView_setStorageEnabled(enabled);
}
public virtual void ZoomIn() {
_assertValidState();
WebView_zoomIn(_nativeWebViewPtr);
}
public virtual void ZoomOut() {
_assertValidState();
WebView_zoomOut(_nativeWebViewPtr);
}
EventHandler _consoleMessageLogged;
protected IntPtr _currentViewportNativeTexture;
#if (UNITY_STANDALONE_WIN && !UNITY_EDITOR) || UNITY_EDITOR_WIN ||UNITY_WEBGL
protected const string _dllName = "VuplexWebViewWindows";
#elif (UNITY_STANDALONE_OSX && !UNITY_EDITOR) || UNITY_EDITOR_OSX
protected const string _dllName = "VuplexWebViewMac";
#elif UNITY_WSA
protected const string _dllName = "VuplexWebViewUwp";
#elif UNITY_ANDROID
protected const string _dllName = "VuplexWebViewAndroid";
#else
protected const string _dllName = "__Internal";
#endif
EventHandler _focusedInputFieldChanged;
FocusedInputFieldType _focusedInputFieldType = FocusedInputFieldType.None;
protected float _height; // in Unity units
Material _materialForBlitting;
// Height in pixels.
protected int _nativeHeight {
get {
// Height must be non-zero
return Math.Max(1, (int)(_height * _numberOfPixelsPerUnityUnit));
}
}
protected IntPtr _nativeWebViewPtr = IntPtr.Zero;
// Width in pixels.
protected int _nativeWidth {
get {
// Width must be non-zero
return Math.Max(1, (int)(_width * _numberOfPixelsPerUnityUnit));
}
}
protected float _numberOfPixelsPerUnityUnit = Config.NumberOfPixelsPerUnityUnit;
List _pageLoadScripts = new List();
List> _pendingCanGoBackCallbacks = new List>();
List> _pendingCanGoForwardCallbacks = new List>();
protected Dictionary> _pendingJavaScriptResultCallbacks = new Dictionary>();
static readonly Regex STREAMING_ASSETS_URL_REGEX = new Regex(@"^streaming-assets:(//)?(.*)$", RegexOptions.IgnoreCase);
const string USER_AGENT_EXCEPTION_MESSAGE = "Unable to set the User-Agent string, because a webview has already been created with the default User-Agent. On Windows and macOS, SetUserAgent() can only be called prior to creating any webviews.";
Rect _videoRect = Rect.zero;
protected Texture2D _videoTexture;
protected bool _viewUpdatesAreEnabled = true;
protected Texture2D _viewportTexture;
protected float _width; // in Unity units
protected void _assertValidState(){
if (!IsInitialized) {
throw new InvalidOperationException("Methods cannot be called on an uninitialized webview. Please initialize it first with IWebView.Init().");
}
if (IsDisposed) {
throw new InvalidOperationException("Methods cannot be called on a disposed webview.");
}
}
protected virtual Material _createMaterialForBlitting() {
return Utils.CreateDefaultMaterial();
}
Texture2D _getReadableTexture() {
// https://support.unity3d.com/hc/en-us/articles/206486626-How-can-I-get-pixels-from-unreadable-textures-
// Create a temporary RenderTexture of the same size as the texture
RenderTexture tempRenderTexture = RenderTexture.GetTemporary(
_nativeWidth,
_nativeHeight,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear
);
if (_materialForBlitting == null) {
_materialForBlitting = _createMaterialForBlitting();
}
// Use the version of Graphics.Blit() that accepts a material
// so that any transformations needed are performed with the shader.
Graphics.Blit(Texture,tempRenderTexture,_materialForBlitting);
// Backup the currently set RenderTexture
RenderTexture previousRenderTexture = RenderTexture.active;
// Set the current RenderTexture to the temporary one we created
RenderTexture.active = tempRenderTexture;
// Create a new readable Texture2D to copy the pixels to it
Texture2D readableTexture = new Texture2D(_nativeWidth, _nativeHeight);
// Copy the pixels from the RenderTexture to the new Texture
readableTexture.ReadPixels(new Rect(0, 0, tempRenderTexture.width, tempRenderTexture.height), 0, 0);
readableTexture.Apply();
// Reset the active RenderTexture
RenderTexture.active = previousRenderTexture;
// Release the temporary RenderTexture
RenderTexture.ReleaseTemporary(tempRenderTexture);
return readableTexture;
}
void _getSelectedText(Action callback) {
// window.getSelection() doesn't work on the content of