下载大文件

8
通过使用 UnityEngine.WWW 下载文件时,我遇到了以下错误:

OverflowException: Number overflow.

经过查找,我发现这个错误是由于结构本身引起的,因为字节数组比 int.MaxValue 可分配的内存 (~2GB) 要多。

通过返回带有 www.bytes 的数组触发了该错误,这意味着框架可能以其他方式存储了该数组。

如何以其他方式访问下载的数据或者是否有更好的替代方案来处理大文件?

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
    }
}

1
这是什么类型的文件?你是在尝试保存它吗?请注意最好也提到你的Unity版本。 - Programmer
这是一个视频文件(mp4),但错误与类型无关,因为它适用于所有文件类型。我使用的版本是2017.3.1f1。 我使用File.WriteAllBytes(path,data)保存它,但无法从www访问数组本身。 - ich
好的,但是你下载完后想用它做什么?播放还是保存?答案取决于这个。 - Programmer
起初我先存储它,然后加载它进行视频播放。直到2GB为止它都可以良好运作。 - ich
1个回答

16

新的答案(Unity 2017.2及以上版本):

使用UnityWebRequestDownloadHandlerFileDownloadHandlerFile类是新的,用于直接下载和保存文件,同时避免高内存使用。

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);
}

旧版答案 (Unity 2017.1及以下版本)(如果要在文件下载时访问每个字节,请使用此选项)

这种问题是 Unity 的 UnityWebRequest 产生的原因,但它不能直接使用,因为最新版本的 Unity 中的 WWW API 是在 UnityWebRequest API 之上实现的,这意味着如果您使用 WWW API 发生错误,则也可能会出现同样的错误 UnityWebRequest。即使它可以工作,您也可能在像 Android 这样内存较小的移动设备上遇到问题。

解决方法是使用 UnityWebRequest 的 DownloadHandlerScript 功能,该功能允许您以块状形式下载数据。通过以块状形式下载数据,您可以防止引起溢出错误。 WWW API 没有实现此功能,因此必须使用 UnityWebRequestDownloadHandlerScript 来以块状形式下载数据。您可以在此处了解其工作原理。

虽然这应该解决您当前的问题,但在尝试使用 File.WriteAllBytes 保存大量数据时,您可能会遇到另一个内存问题。使用 FileStream 来执行保存部分,并仅在下载完成后关闭它。

创建一个自定义的 UnityWebRequest 以按块下载数据:

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));
    }
}

使用方法:

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;
}

1
谢谢。我做了一些更改,但这个解决方案很好用。谢谢! - ich
能否从字节中检索请求的状态码?因为如果文件不存在,它会存储一对字节的文件。我在ReceiveData中尝试过了。 - ich
1
你的意思是在保存文件之前检查服务器上是否存在该文件?听起来很复杂,我认为你应该创建一个新的问题来询问。我会尝试想出一些解决方案。 - Programmer
我找到了一个解决方案。我添加了一个布尔值,用于检查文件流是否已创建,并将其移动到AppendFile的开头。在receiveData中,我检查数据长度是否为14(文件未找到的错误消息的长度),如果是,则检查System.Text.Encoding.Default.GetString(byteFromServer)是否包含“File not found”,并在这种情况下返回false和失败的结果。 - ich
1
请查看我的回答中的编辑。removeFileOnAbort 可能会在使用新的 DownloadHandlerFile API 时有用。祝你好运。 - Programmer
1
谢谢,我现在测试了新的答案,它运行良好。谢谢! - ich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接