AssetRipper / AssetRipper

GUI Application to work with engine assets, asset bundles, and serialized files
https://assetripper.github.io/AssetRipper/
GNU General Public License v3.0
3.8k stars 518 forks source link

[Enhancement]: Optimize blob assets transfer #1250

Closed Sieluna closed 5 months ago

Sieluna commented 5 months ago

Describe the new feature or enhancement

Transmitting large assets directly through documents is not a perfect solution because base64, which increases the size about 20%, besides this will increase page rendering time as well. I think some gentle transmission can be considered. The following is a draft for transmission through websocket.

https://github.com/AssetRipper/AssetRipper/blob/ef00ea8494ac685b27f18b648b6387084f08b9ae/Source/AssetRipper.GUI.Web/WebApplicationLauncher.cs#L215

+  //Large Blob Transfer
+  app.UseWebSockets();
+  app.Use(async (context, next) =>
+  {
+    if (context.WebSockets.IsWebSocketRequest)
+    {
+      IWebSocketHandler handler = new WebSocketLoader().GetHandler(context);
+      WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
+      await handler.HandleAsync(webSocket);
+    }
+    else
+    {
+      await next();
+    }
+  });
using AssetRipper.Assets;
using AssetRipper.GUI.Web.Paths;
using Microsoft.AspNetCore.Http;
using System.Net.WebSockets;

namespace AssetRipper.GUI.Web
{
    public interface IWebSocketHandler
    {
        Task HandleAsync(WebSocket webSocket);
    }

    public class NoopWebSocketHandler : IWebSocketHandler
    {
        public string Message { private get; set; } = "Empty router, the connection close immediately.";

        public async Task HandleAsync(WebSocket webSocket)
        {
            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, Message, CancellationToken.None);
        }
    }

    public class WebSocketLoader
    {
        // TODO: The code here should use more dynamic loading method, such as attribute + source gen
        public IWebSocketHandler GetHandler(HttpContext context)
        {
            string? json = context.Request.Form[PathLinking.FormKey];
            if (string.IsNullOrEmpty(json))
            {
                return new NoopWebSocketHandler { Message = "The path must be included in the request." };
            }

            AssetPath path;
            try
            {
                path = AssetPath.FromJson(json);
            }
            catch (Exception ex)
            {
                return new NoopWebSocketHandler { Message = ex.ToString() };
            }

            if (!GameFileLoader.IsLoaded)
            {
                return new NoopWebSocketHandler { Message = "No files loaded." };
            }

            if (!GameFileLoader.GameBundle.TryGetAsset(path, out IUnityObjectBase? asset))
            {
                return new NoopWebSocketHandler { Message = $"Asset could not be resolved: {path}" };
            }

            switch (context.Request.Path.ToString().ToLower())
            {
                // case "MYASSETSTYPEURL":
                //  return new XXXXXWebSocketHandler { Asset = asset };
                default:
                    return new NoopWebSocketHandler();
            }
        }
    }
}

Some possible usage:

class AudioWebSocketHandler : IWebSocketHandler
{
    public required IUnityObjectBase Asset { get; init; }

    public async Task HandleAsync(WebSocket webSocket)
    {
        byte[] buffer = new byte[1024 * 4];
        await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

        if (Asset is IAudioClip clip && AudioClipDecoder.TryDecode(clip, out byte[]? decodedAudioData, out string? extension, out _))
        {
            if (webSocket.State != WebSocketState.Open)
            {
                return;
            }

            ArraySegment<byte> segment = new(decodedAudioData);

            await webSocket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
        }
    }
}
ds5678 commented 5 months ago

In regards to the page rendering time, I want to make it load them asynchronously. Encoding them as base64 was just a way to simplify development and ship a "minimum viable product."

ds5678 commented 5 months ago

The architecture to support asynchronous loading would also let people use AssetRipper more like a library, which is a shadow goal of switching to the web UI.