環境
UniTask 2.0.19
デフォルトではUnityWebRequestをUniTaskで待つときにdownloadedBytesを受け取れない
UnityAsyncExtensions.cs
public static UniTask<UnityWebRequest> ToUniTask(this UnityWebRequestAsyncOperation asyncOperation, IProgress<float> progress = null, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken))
UnityWebRequestをToUniTaskでUniTaskに変換するときにIProgressを使って割合は取ってこれるが、downloadedBytesは取れない。
(Content-Lengthでダウンロードサイズ取得してProgress使って算出する等は可能だと思うが、UnityWebRequestのdownloadedBytesが取れないって意味で)
そこで、引数にAction<long> reportDownloadedBytes
を追加してみる。
新しく拡張メソッドを定義
UnityWebRequestAsyncOperationExt.cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine.Networking;
namespace DL
{
public static partial class UnityWebRequestAsyncOperationExt
{
public static UniTask<UnityWebRequest> ToUniTaskWithDownloadedBytes(this UnityWebRequestAsyncOperation asyncOperation, IProgress<float> progress = null, Action<ulong> reportDownloadedBytes = null, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken))
{
if (asyncOperation == null)
{
throw new ArgumentNullException(nameof(asyncOperation));
}
if (asyncOperation.isDone) return UniTask.FromResult(asyncOperation.webRequest);
return new UniTask<UnityWebRequest>(UnityWebRequestAsyncOperationConfiguredSource.Create(asyncOperation, timing, progress, reportDownloadedBytes, cancellationToken, out var token), token);
}
sealed class UnityWebRequestAsyncOperationConfiguredSource : IUniTaskSource<UnityWebRequest>, IPlayerLoopItem, ITaskPoolNode<UnityWebRequestAsyncOperationConfiguredSource>
{
static TaskPool<UnityWebRequestAsyncOperationConfiguredSource> pool;
public UnityWebRequestAsyncOperationConfiguredSource NextNode { get; set; }
static UnityWebRequestAsyncOperationConfiguredSource()
{
TaskPool.RegisterSizeGetter(typeof(UnityWebRequestAsyncOperationConfiguredSource), () => pool.Size);
}
UnityWebRequestAsyncOperation asyncOperation;
IProgress<float> progress;
Action<ulong> reportDownloadedBytes;
CancellationToken cancellationToken;
UniTaskCompletionSourceCore<UnityWebRequest> core;
UnityWebRequestAsyncOperationConfiguredSource()
{
}
public static IUniTaskSource<UnityWebRequest> Create(UnityWebRequestAsyncOperation asyncOperation, PlayerLoopTiming timing, IProgress<float> progress, Action<ulong> reportDownloadedBytes, CancellationToken cancellationToken, out short token)
{
if (cancellationToken.IsCancellationRequested)
{
return AutoResetUniTaskCompletionSource<UnityWebRequest>.CreateFromCanceled(cancellationToken, out token);
}
if (!pool.TryPop(out var result))
{
result = new UnityWebRequestAsyncOperationConfiguredSource();
}
result.asyncOperation = asyncOperation;
result.progress = progress;
result.reportDownloadedBytes = reportDownloadedBytes;
result.cancellationToken = cancellationToken;
TaskTracker.TrackActiveTask(result, 3);
PlayerLoopHelper.AddAction(timing, result);
token = result.core.Version;
return result;
}
public UnityWebRequest GetResult(short token)
{
try
{
return core.GetResult(token);
}
finally
{
TryReturn();
}
}
void IUniTaskSource.GetResult(short token)
{
GetResult(token);
}
public UniTaskStatus GetStatus(short token)
{
return core.GetStatus(token);
}
public UniTaskStatus UnsafeGetStatus()
{
return core.UnsafeGetStatus();
}
public void OnCompleted(Action<object> continuation, object state, short token)
{
core.OnCompleted(continuation, state, token);
}
public bool MoveNext()
{
if (cancellationToken.IsCancellationRequested)
{
asyncOperation.webRequest.Abort();
core.TrySetCanceled(cancellationToken);
return false;
}
if (progress != null)
{
progress.Report(asyncOperation.progress);
}
if (reportDownloadedBytes != null)
{
reportDownloadedBytes.Invoke(asyncOperation.webRequest.downloadedBytes);
}
if (asyncOperation.isDone)
{
if (asyncOperation.webRequest.isHttpError || asyncOperation.webRequest.isNetworkError)
{
core.TrySetException(new UnityWebRequestException(asyncOperation.webRequest));
}
else
{
core.TrySetResult(asyncOperation.webRequest);
}
return false;
}
return true;
}
bool TryReturn()
{
TaskTracker.RemoveTracking(this);
core.Reset();
asyncOperation = default;
progress = default;
cancellationToken = default;
return pool.TryPush(this);
}
}
}
}
内容はほぼUniTaskのをコピペ。
UnityWebRequestAsyncOperationConfiguredSourceのMoveNextでprogress.Report
を呼んでいたため、
その下にreportDownloadedBytesを追加。
テスト
UnityWebRequestTest.cs
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.TestTools;
using Cysharp.Threading.Tasks;
using System;
using NUnit.Framework;
using System.IO;
using DL;
namespace DLTests
{
public class UnityWebRequestTest
{
[UnityTest]
public IEnumerator DownloadFileTest() => UniTask.ToCoroutine(async () =>
{
string testDir = "Test/DL/DownloadFileTest";
string url = "https://teach310.github.io/AssetBundlesForDownloadTest/SBP/Prefabs001";
string destPath = testDir + "/Prefabs001";
float timeout = 3f;
// リセット
if (Directory.Exists(testDir))
{
Directory.Delete(testDir, true);
}
Directory.CreateDirectory(testDir);
ulong downloadedBytes = 0L;
Action<ulong> reportDownloadedBytes = x => downloadedBytes = x;
using (var request = UnityWebRequest.Get(url))
{
var downloadHandler = new DownloadHandlerFile(destPath);
downloadHandler.removeFileOnAbort = true;
request.downloadHandler = downloadHandler;
try
{
await request.SendWebRequest()
.ToUniTaskWithDownloadedBytes(reportDownloadedBytes: reportDownloadedBytes)
.Timeout(TimeSpan.FromSeconds(timeout));
}
catch (UnityWebRequestException ex)
{
if (ex.IsHttpError)
{
Assert.Fail($"HTTPError status {request.responseCode} message {ex.Message}");
}
else if (ex.IsNetworkError)
{
Assert.Fail($"NetworkError status {request.responseCode} message {ex.Message}");
}
Assert.Fail(ex.Message);
}
catch (TimeoutException ex)
{
Assert.Fail(ex.Message);
}
catch (Exception ex)
{
// urlが間違ってるときとかに呼ばれる。
Assert.Fail(ex.Message);
}
}
Assert.AreEqual(true, downloadedBytes > 0L);
Directory.Delete(testDir, true);
});
}
}
リクエストがIsHttpErrorまたはIsNetworkErrorのときにUnityWebRequestExceptionをthrowするようになっていたことに気づけてよかった。