I am trying to figure out how to stop UnityWebRequest download immediately with a button in android application.
Stopping coroutine with StopCoroutine("downLoadFromServer"); does not stop UnityWebRequest.
I tried using UnityWebRequest.Dispose(); or UnityWebRequest.Abort(); with no success.
Below is the download i'm trying to stop.
private IEnumerator downLoadFromServer()
{
var url = "https://example.com/app.apk";
var savePath = Path.Combine(Application.persistentDataPath, "data", "app.apk");
using (var uwr = UnityWebRequest.Get(url))
{
uwr.SetRequestHeader("Authorization", "Basic " + System.Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes("test:test")));
uwr.SendWebRequest();
while (!uwr.isDone)
{
progressText.text = $"Progress: {uwr.downloadProgress:P}";
yield return null;
}
var yourBytes = uwr.downloadHandler.data;
progressText.text = $"Done downloading. Size: {yourBytes.Length}";
//Create Directory if it does not exist
var directoryName = Path.GetDirectoryName(savePath);
if (!Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
progressText.text = "Created Dir";
}
try
{
//Now Save it
System.IO.File.WriteAllBytes(savePath, yourBytes);
Debug.Log("Saved Data to: " + savePath.Replace("/", "\\"));
progressText.text = "Saved Data";
}
catch (Exception e)
{
Debug.LogWarning("Failed To Save Data to: " + savePath.Replace("/", "\\"));
Debug.LogWarning("Error: " + e.Message);
progressText.text = "Error Saving Data";
}
}
//Install APK
installApp(savePath);
}
Thank you,
Chris
You are right: Stopping the Coroutine does not cancel the UnityWebRequest, you are just not keeping track of it anymore.
Actually UnityWebRequest.Abort should do exactly that
If in progress, halts the UnityWebRequest as soon as possible.
This method may be called at any time. If the UnityWebRequest has not already completed, the UnityWebRequest will halt uploading or downloading data as soon as possible. Aborted UnityWebRequests are considered to have encountered a system error. Either the isNetworkError or the isHttpError property will return true and the error property will be "User Aborted".
However this makes ofcourse only sense if you actually check the result of the request which currently you aren't doing and you should do anyway!
A way to use it would be to make the uwr a class wide field so you can access it from somewhere else and as said actually add a check if the download succeeded:
private UnityWebRequest uwr;
private IEnumerator downLoadFromServer()
{
...
// NOTE: Remove the var so the class field uwr is assigned to, not only
// a local variable
using(uwr = UnityWebRequest.Get(...))
{
...
// Always check if a request was actually successful before continuing
if(uwr.isHttpError || uwr.isNetworkError || !string.IsNullOrWhiteSpace(uwr.error))
{
Debug.LogWarning($"Download Failed with {uwr.responseCode}, reason: {uwr.error}", this);
progressText.text = $"Download Failed: {uwr.error}";
// Cancel this Coroutine
yield break;
}
var yourBytes = uwr.downloadHandler.data;
...
}
//Install APK
installApp(savePath);
}
Then you can simply do
public void AbortDownload()
{
if(uwr != null && !uwr.isDone)
{
Debug.Log("Aborting download ...", this);
uwr.Abort();
}
else
{
Debug.Log("Not downloading, nothing to do ...", this);
}
}
which should make the download "fail" and thereby also finish the Coroutine
Related
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
}
}
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;
}
In my script im expecting the text bytesDownloadedText to be updated with how many bytes have been downloaded so far but it only runs once and stays on 0. How do I fix this
private IEnumerator DownloadFile(){
WWW w = new WWW (PATH_TO_DOWNLOAD);
bytesDownloadedText.text = w.bytesDownloaded.ToString();
yield return w;
if (w.error != null) {
Debug.LogError ("Error: " + w.error);
} else {
scriptText = w.text;
filesDownloaded = true;
Debug.Log (scriptText);
}
}
Edit: New code + Additional debugging information
private IEnumerator DownloadFile(){
WWW w = new WWW (PATH_TO_DOWNLOAD);
while(!w.isDone){
bytesDownloadedText.text = w.bytesDownloaded.ToString ();
Debug.Log ("Bytes Downloaded: " + w.bytesDownloaded);
yield return null;
}
Debug.Log ("Exiting while loop");
if (w.error != null) {
Debug.LogError ("Error: " + w.error);
} else {
//bytesDownloadedText.text = w.bytesDownloaded.ToString ();
scriptText = w.text;
filesDownloaded = true;
Debug.Log (scriptText);
}
}
Filesize: 337 bytes
Download Path: https://deathcrow.altervista.org/websharp/files.php
3.Code where DownloadFile is called
private void Start(){
StartCoroutine (DownloadFile ());
}
The while loop is exiting
w.text returns the test script from the server(which is code)
using UnityEngine;
public class TestScript: MonoBehaviour{
public void Update(){
if (Input.GetMouseButtonDown (0)) {
RaycastHit hit;
if (Physics.Raycast (Camera.main.ScreenPointToRay(Input.mousePosition),out hit)){
if (hit.transform.tag == "Destroy") {
Destroy (hit.transform.root.gameObject);
}
}
}
}
}
Edit: Online code
<?
$scriptText = "";
$file_handle = fopen("TestScript.cs","r");
while(!feof($file_handle)){
$line = fgets($file_handle);
$scriptText = $scriptText . $line;
}
fclose($file_handle);
$size = strlen($scriptText);
header("Content-length: ".$size);
echo $scriptText;
?>
Do not yield WWW if you want to use the bytesDownloaded property as that will pause your code until WWW returns which makes it impossible to read how much data has been downloaded.
You have to put WWW.bytesDownloaded in a loop then use WWW.isDone to detect when WWW is done and then exit the loop. Inside that loop, you can use WWW.bytesDownloaded to display the downloaded data. Finally, you must wait for a frame after each loop so that other scripts can execute or Unity will freeze until the download is done.
This is what that code should look like:
private IEnumerator DownloadFile()
{
WWW w = new WWW(PATH_TO_DOWNLOAD);
while (!w.isDone)
{
yield return null;
bytesDownloadedText.text = w.bytesDownloaded.ToString();
Debug.Log("Bytes Downloaded: " + w.bytesDownloaded);
}
if (w.error != null)
{
Debug.LogError("Error: " + w.error);
}
else
{
scriptText = w.text;
filesDownloaded = true;
Debug.Log(scriptText);
}
}
Note: There are some instances where the bytesDownloaded property returns 0. This has nothing to do with Unity. This happens mostly when your server is not sending the Content-Length header.
Example of sending the Content-Length header from the server(php):
<?php
//String to send
$data = "Test message to send";
//Get size
$size= strlen($data);
//Set Content-length header
header("Content-length: ".$size);
//Finally, you can send the data
echo $data;
?>
Right now, it seems UnityWebRequest.GetAssetBundledownload asset to RAM and load. Is there anyway to download to hard disk to load?
This is not really complicated. Handle it like a normal file you would download from the internet but save it with the ".unity3d" extension.
1.Download the AssetBundle as a normal file by making a request with UnityWebRequest.
UnityWebRequest www = UnityWebRequest.Get(url);
yield return www.Send();
2.Retrieve the byte array data with DownloadHandler.data then save it to Application.persistentDataPath/yourfolder/filename.unity3d. Make sure that the extension is ".unity3d".
File.WriteAllBytes(handle.data, data);
That's it.
3.To load the data, use AssetBundle.LoadFromFile or AssetBundle.LoadFromFileAsync:
AssetBundleCreateRequest bundle = AssetBundle.LoadFromFileAsync(path);
If still confused, here is what the thing should look like. You may need to make some modification:
Download and Save:
IEnumerator downloadAsset()
{
string url = "http://url.net/YourAsset.unity3d";
UnityWebRequest www = UnityWebRequest.Get(url);
DownloadHandler handle = www.downloadHandler;
//Send Request and wait
yield return www.Send();
if (www.isError)
{
UnityEngine.Debug.Log("Error while Downloading Data: " + www.error);
}
else
{
UnityEngine.Debug.Log("Success");
//handle.data
//Construct path to save it
string dataFileName = "WaterVehicles";
string tempPath = Path.Combine(Application.persistentDataPath, "AssetData");
tempPath = Path.Combine(tempPath, dataFileName + ".unity3d");
//Save
save(handle.data, tempPath);
}
}
void save(byte[] data, string path)
{
//Create the Directory if it does not exist
if (!Directory.Exists(Path.GetDirectoryName(path)))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
}
try
{
File.WriteAllBytes(path, data);
Debug.Log("Saved Data to: " + path.Replace("/", "\\"));
}
catch (Exception e)
{
Debug.LogWarning("Failed To Save Data to: " + path.Replace("/", "\\"));
Debug.LogWarning("Error: " + e.Message);
}
}
Load:
IEnumerable LoadObject(string path)
{
AssetBundleCreateRequest bundle = AssetBundle.LoadFromFileAsync(path);
yield return bundle;
AssetBundle myLoadedAssetBundle = bundle.assetBundle;
if (myLoadedAssetBundle == null)
{
Debug.Log("Failed to load AssetBundle!");
yield break;
}
AssetBundleRequest request = myLoadedAssetBundle.LoadAssetAsync<GameObject>("boat");
yield return request;
GameObject obj = request.asset as GameObject;
obj.transform.position = new Vector3(0.08f, -2.345f, 297.54f);
obj.transform.Rotate(350.41f, 400f, 20f);
obj.transform.localScale = new Vector3(1.0518f, 0.998f, 1.1793f);
Instantiate(obj);
myLoadedAssetBundle.Unload(false);
}
Right now, it seems UnityWebRequest.GetAssetBundledownload asset to RAM and load. Is there anyway to download to hard disk to load?
This is not really complicated. Handle it like a normal file you would download from the internet but save it with the ".unity3d" extension.
1.Download the AssetBundle as a normal file by making a request with UnityWebRequest.
UnityWebRequest www = UnityWebRequest.Get(url);
yield return www.Send();
2.Retrieve the byte array data with DownloadHandler.data then save it to Application.persistentDataPath/yourfolder/filename.unity3d. Make sure that the extension is ".unity3d".
File.WriteAllBytes(handle.data, data);
That's it.
3.To load the data, use AssetBundle.LoadFromFile or AssetBundle.LoadFromFileAsync:
AssetBundleCreateRequest bundle = AssetBundle.LoadFromFileAsync(path);
If still confused, here is what the thing should look like. You may need to make some modification:
Download and Save:
IEnumerator downloadAsset()
{
string url = "http://url.net/YourAsset.unity3d";
UnityWebRequest www = UnityWebRequest.Get(url);
DownloadHandler handle = www.downloadHandler;
//Send Request and wait
yield return www.Send();
if (www.isError)
{
UnityEngine.Debug.Log("Error while Downloading Data: " + www.error);
}
else
{
UnityEngine.Debug.Log("Success");
//handle.data
//Construct path to save it
string dataFileName = "WaterVehicles";
string tempPath = Path.Combine(Application.persistentDataPath, "AssetData");
tempPath = Path.Combine(tempPath, dataFileName + ".unity3d");
//Save
save(handle.data, tempPath);
}
}
void save(byte[] data, string path)
{
//Create the Directory if it does not exist
if (!Directory.Exists(Path.GetDirectoryName(path)))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
}
try
{
File.WriteAllBytes(path, data);
Debug.Log("Saved Data to: " + path.Replace("/", "\\"));
}
catch (Exception e)
{
Debug.LogWarning("Failed To Save Data to: " + path.Replace("/", "\\"));
Debug.LogWarning("Error: " + e.Message);
}
}
Load:
IEnumerable LoadObject(string path)
{
AssetBundleCreateRequest bundle = AssetBundle.LoadFromFileAsync(path);
yield return bundle;
AssetBundle myLoadedAssetBundle = bundle.assetBundle;
if (myLoadedAssetBundle == null)
{
Debug.Log("Failed to load AssetBundle!");
yield break;
}
AssetBundleRequest request = myLoadedAssetBundle.LoadAssetAsync<GameObject>("boat");
yield return request;
GameObject obj = request.asset as GameObject;
obj.transform.position = new Vector3(0.08f, -2.345f, 297.54f);
obj.transform.Rotate(350.41f, 400f, 20f);
obj.transform.localScale = new Vector3(1.0518f, 0.998f, 1.1793f);
Instantiate(obj);
myLoadedAssetBundle.Unload(false);
}
How do I send a PUT request using UnityWebRequest?
The PUT is being sent; Uploaded!! is being printed to the console. However, nothing is being updated. I think I'm formatting myData incorrectly.
The actual URL I'm trying to send the PUT to is formatted like... http://servername.com/api/dogs/1/token=fndskajfdafdsf&cleanliness_level=20
Sorry, I can't remember what that format is called.
This is the code that I have:
public string url = "http://servername.com/api/dogs/1";
.
.
.
void Start() {
StartCoroutine (UpdateDogs ("clean"));
}
IEnumerator UpdateDogs (string button)
{
byte[] myData;
if (button == "feed") {
myData = System.Text.Encoding.UTF8.GetBytes ("?token=" + token + "&health_level=" + healthLevel);
} else {
myData = System.Text.Encoding.UTF8.GetBytes ("?token=" + token + "&cleanliness_level=" + cleanlinessLevel);
}
using (UnityWebRequest www = UnityWebRequest.Put (url, myData)) {
yield return www.Send ();
if (www.isError) {
Debug.Log ("PUT ERROR: " + www.error);
} else {
Debug.Log ("Uploaded!!");
}
}
It looks like you're missing an & in the parameter string after the token.
token + "cleanliness_level=
Should be
token + "&cleanliness_level=
Similarly for the other possible parameters.
myData in UnityWebRequest.Put is only for http body data. It doesn't seem like you need that. Instead you should add your query parameters to the URL, like such:
public string url = "http://servername.com/api/dogs/1";
if (button == "feed") {
url += "?token=" + token + "&health_level=" + healthLevel;
} else {
url += "?token=" + token + "&cleanliness_level=" + cleanlinessLevel;
}
using (UnityWebRequest www = UnityWebRequest.Put (url, "dummy")) { // UnityWebRequest.Put requires a body, see comments below
yield return www.Send ();
if (www.isError) {
Debug.Log ("PUT ERROR: " + www.error);
} else {
Debug.Log ("Uploaded!!");
}
}
I think you are missing a forward slash before token when you send it.
Try debugging what you actually send with
Debug.Log (www.text);
Next advice is that maybe the error is with what code you use on the back end receiving the data.