8

By downloading a file with UnityEngine.WWW, I get the error

OverflowException: Number overflow.

I found out the error is caused form the structure itself, because the byte-array has more bytes than int.MaxValue can allocate (~2GB).

The error is fired by returning the array with www.bytes, which means, that the framework probably stores the array in on other way.

How can I access the downloaded data in another way or is there an alternative for bigger files?

public IEnumerator downloadFile()
{
    WWW www = new WWW(filesource);

    while(!www.isDone)
    {
        progress = www.progress;
        yield return null;
    }

    if(string.IsNullOrEmpty(www.error))
    {
        data = www.bytes; // <- Errormessage fired here
    }
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
ich
  • 135
  • 2
  • 10
  • 1
    What type of file is this and are you trying to save it after ? Note that it would be great to also mention your Unity version – Programmer Jun 04 '18 at 23:10
  • Its a videofile (mp4), but the error is indipendend from the type, because it happens with all filetypes. I use version 2017.3.1f1 I save it with File.WriteAllBytes(path, data), but its not possible to access the Array itself from www – ich Jun 04 '18 at 23:13
  • Ok but what you trying to do with it after downloading it? Play it? Save it? Answer depends on that – Programmer Jun 04 '18 at 23:14
  • At first I store it and load it after that for video-playback. It works well up to 2GB. – ich Jun 04 '18 at 23:16

1 Answers1

16

New answer (Unity 2017.2 and above)

Use UnityWebRequest with DownloadHandlerFile. The DownloadHandlerFile class is new and is used to download and save file directly while preventing high memory usage.

IEnumerator Start()
{
    string url = "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm";

    string vidSavePath = Path.Combine(Application.persistentDataPath, "Videos");
    vidSavePath = Path.Combine(vidSavePath, "MyVideo.webm");

    //Create Directory if it does not exist
    if (!Directory.Exists(Path.GetDirectoryName(vidSavePath)))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(vidSavePath));
    }

    var uwr = new UnityWebRequest(url);
    uwr.method = UnityWebRequest.kHttpVerbGET;
    var dh = new DownloadHandlerFile(vidSavePath);
    dh.removeFileOnAbort = true;
    uwr.downloadHandler = dh;
    yield return uwr.SendWebRequest();

    if (uwr.isNetworkError || uwr.isHttpError)
        Debug.Log(uwr.error);
    else
        Debug.Log("Download saved to: " + vidSavePath.Replace("/", "\\") + "\r\n" + uwr.error);
}

OLD answer (Unity 2017.1 and below) Use if you want to access each bytes while the file is downloading)

A problem like this is why Unity's UnityWebRequest was made but it won't work directly because WWW API is now implemented on top of the UnityWebRequest API in the newest version of Unity which means that if you get error with the WWW API, you will also likely get that same error with UnityWebRequest. Even if it works, you'll likely have have issues on mobile devices with the small ram like Android.

What to do is use UnityWebRequest's DownloadHandlerScript feature which allows you to download data in chunks. By downloading data in chunks, you can prevent causing the overflow error. The WWW API did not implement this feature so UnityWebRequest and DownloadHandlerScript must be used to download the data in chunks. You can read how this works here.

While this should solve your current issue, you may run into another memory issue when trying to save that large data with File.WriteAllBytes. Use FileStream to do the saving part and close it only when the download has finished.

Create a custom UnityWebRequest for downloading data in chunks as below:

using System;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

public class CustomWebRequest : DownloadHandlerScript
{
    // Standard scripted download handler - will allocate memory on each ReceiveData callback
    public CustomWebRequest()
        : base()
    {
    }

    // Pre-allocated scripted download handler
    // Will reuse the supplied byte array to deliver data.
    // Eliminates memory allocation.
    public CustomWebRequest(byte[] buffer)
        : base(buffer)
    {

        Init();
    }

    // Required by DownloadHandler base class. Called when you address the 'bytes' property.
    protected override byte[] GetData() { return null; }

    // Called once per frame when data has been received from the network.
    protected override bool ReceiveData(byte[] byteFromServer, int dataLength)
    {
        if (byteFromServer == null || byteFromServer.Length < 1)
        {
            Debug.Log("CustomWebRequest :: ReceiveData - received a null/empty buffer");
            return false;
        }

        //Write the current data chunk to file
        AppendFile(byteFromServer, dataLength);

        return true;
    }

    //Where to save the video file
    string vidSavePath;
    //The FileStream to save the file
    FileStream fileStream = null;
    //Used to determine if there was an error while opening or saving the file
    bool success;

    void Init()
    {
        vidSavePath = Path.Combine(Application.persistentDataPath, "Videos");
        vidSavePath = Path.Combine(vidSavePath, "MyVideo.webm");


        //Create Directory if it does not exist
        if (!Directory.Exists(Path.GetDirectoryName(vidSavePath)))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(vidSavePath));
        }


        try
        {
            //Open the current file to write to
            fileStream = new FileStream(vidSavePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
            Debug.Log("File Successfully opened at" + vidSavePath.Replace("/", "\\"));
            success = true;
        }
        catch (Exception e)
        {
            success = false;
            Debug.LogError("Failed to Open File at Dir: " + vidSavePath.Replace("/", "\\") + "\r\n" + e.Message);
        }
    }

    void AppendFile(byte[] buffer, int length)
    {
        if (success)
        {
            try
            {
                //Write the current data to the file
                fileStream.Write(buffer, 0, length);
                Debug.Log("Written data chunk to: " + vidSavePath.Replace("/", "\\"));
            }
            catch (Exception e)
            {
                success = false;
            }
        }
    }

    // Called when all data has been received from the server and delivered via ReceiveData
    protected override void CompleteContent()
    {
        if (success)
            Debug.Log("Done! Saved File to: " + vidSavePath.Replace("/", "\\"));
        else
            Debug.LogError("Failed to Save File to: " + vidSavePath.Replace("/", "\\"));

        //Close filestream
        fileStream.Close();
    }

    // Called when a Content-Length header is received from the server.
    protected override void ReceiveContentLength(int contentLength)
    {
        //Debug.Log(string.Format("CustomWebRequest :: ReceiveContentLength - length {0}", contentLength));
    }
}

How to use:

UnityWebRequest webRequest;
//Pre-allocate memory so that this is not done each time data is received
byte[] bytes = new byte[2000];

IEnumerator Start()
{
    string url = "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm";
    webRequest = new UnityWebRequest(url);
    webRequest.downloadHandler = new CustomWebRequest(bytes);
    webRequest.SendWebRequest();
    yield return webRequest;
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • 1
    Thanks. I made some changes, but this solution works fine. Thank you! – ich Jun 05 '18 at 01:10
  • Is it possible to retrieve from the bytes the status code of the request?because if the file no exists, it stores the file with a couple of byte. I tried it in ReceiveData – ich Jun 07 '18 at 22:15
  • 1
    You mean to check if the file exist on the server before saving it? That sounds complicated and I think you should create new question about it. I will try to come up with something. – Programmer Jun 08 '18 at 01:15
  • I've found a solution. I added a bool if filestream has been created, moved the creation at the beginning of AppendFile. In receiveData I check if the datalength is 14 (length of the errorMessage of File not found), check in this case if System.Text.Encoding.Default.GetString(byteFromServer).Contains("File not found") and return in this case false with success false. – ich Jun 08 '18 at 02:16
  • 1
    See my edit in my answer. `removeFileOnAbort` might be useful for this with the new `DownloadHandlerFile` API. Good luck. – Programmer Jun 08 '18 at 02:58
  • 1
    Thank you, I tested now the new answer, it works good. Thank you! – ich Jun 11 '18 at 03:42