Unity3D game freezes when sending an API request - c#

I'm using an open-source game to write a script, that captures the screen frames in a collection and sends the collection to an API server.
Capturing works fine, but as soon as the API request fires, the game freezes for ~500ms. If I understand correctly, this request blocks the main thread.
I've tried to send the request in a different thread, but get some weird errors.
How can I send requests in a background, so it doesn't block the main thread?
Here's my code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using System.Threading.Tasks;
public class Recorder : MonoBehaviour
{
private float captureInterval = 1f / 24f;
private readonly List<string> frames = new();
async void Update()
{
if (Time.timeSinceLevelLoad >= captureInterval)
{
StartCoroutine(CaptureFrame());
captureInterval = Time.timeSinceLevelLoad + (1f / 24f);
if (frames.Count >= 120)
{
StartCoroutine(CommitFrames(frames));
frames.Clear();
}
}
}
IEnumerator CaptureFrame()
{
yield return new WaitForEndOfFrame();
Texture2D texture = ScreenCapture.CaptureScreenshotAsTexture();
byte[] jpg = texture.EncodeToJPG();
string base64 = Convert.ToBase64String(jpg);
frames.Add(base64);
Destroy(texture);
}
IEnumerator CommitFrames(List<string> framesData)
{
var payload = new CommitFramesData() { frames = framesData };
var www = CreateRequest("http://my-api.test/api/frames", RequestType.POST, payload);
yield return www.SendWebRequest();
www.Dispose();
}
private UnityWebRequest CreateRequest(string path, RequestType type = RequestType.GET, object data = null)
{
var request = new UnityWebRequest(path, type.ToString());
if (data != null)
{
var bodyRaw = Encoding.UTF8.GetBytes(JsonUtility.ToJson(data));
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
}
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
return request;
}
}
public enum RequestType
{
GET = 0,
POST = 1,
PUT = 2
}
[Serializable]
class CommitFramesData
{
public List<string> frames;
}

Related

Convert Image to base64 and vice versa in Unity

i wanted to convert this image to Base64 in unity C#
But I am kind of lost, can someone help?
This is my code
using UnityEngine;
using System.Collections;
using System.IO;
using UnityEngine.UI;
using System.Collections.Generic;
using UnityEngine.Networking;
public class GetCam : MonoBehaviour
{
WebCamTexture webCam;
string your_path = "C:\\Users\\Jay\\Desktop";
public RawImage display;
public AspectRatioFitter fit;
public void Start()
{
webCam = new WebCamTexture();
webCam.Play();
if(WebCamTexture.devices.Length==0)
{
Debug.LogError("can not found any camera!");
return;
}
int index = -1;
for (int i = 0; i < WebCamTexture.devices.Length; i++)
{
if (WebCamTexture.devices[i].name.ToLower().Contains("your webcam name"))
{
Debug.LogError("WebCam Name:" + WebCamTexture.devices[i].name + " Webcam Index:" + i);
index = i;
}
}
}
public void Update()
{
float ratio = (float)webCam.width / (float)webCam.height;
fit.aspectRatio = ratio;
float ScaleY = webCam.videoVerticallyMirrored ? -1f : 1f;
display.rectTransform.localScale = new Vector3(1f, ScaleY, 1f);
int orient = -webCam.videoRotationAngle;
display.rectTransform.localEulerAngles = new Vector3(0, 0, orient);
}
public void callTakePhoto()
{
StartCoroutine(TakePhoto());
}
IEnumerator TakePhoto()
{
yield return new WaitForEndOfFrame();
Texture2D photo = new Texture2D(webCam.width, webCam.height);
photo.SetPixels(webCam.GetPixels());
photo.Apply();
//Encode to a PNG
byte[] bytes = photo.EncodeToPNG();
//Convert PNG to Base64
//Write out the PNG. Of course you have to substitute your_path for something sensible
File.WriteAllBytes(your_path + "\\photo.png", bytes);
//lol
}
public void callpostRequest()
{
StartCoroutine(postRequest("http://127.0.0.1:5000/"));
}
IEnumerator postRequest(string url)
{
WWWForm form = new WWWForm();
form.AddField("name","hi from unity client");
// form.AddField("nice","ok");
UnityWebRequest uwr = UnityWebRequest.Post(url, form);
yield return uwr.SendWebRequest();
StartCoroutine(getRequest("http://127.0.0.1:5000/"));
if (uwr.isNetworkError)
{
Debug.Log("Error While Sending: " + uwr.error);
}
else
{
Debug.Log("Received: " + uwr.downloadHandler.text);
}
}
IEnumerator getRequest(string uri)
{
UnityWebRequest uwr = UnityWebRequest.Get(uri);
yield return uwr.SendWebRequest();
if (uwr.isNetworkError)
{
Debug.Log("Error While Sending: " + uwr.error);
}
else
{
Debug.Log("Received: " + uwr.downloadHandler.text);
}
}
}
For now all it does is click picture and save it to desktop and then below is the code of UnityWebRequest class which sends a hi message to a flask server and receives back the same
What i want to do is convert image to base64 and send to flask server, can anyone help? thank you
In general you would simply do Convert.ToBase64String(byte[])
string base64String = Convert.ToBase64String(byteArray);
and accordingly Convert.FromBase64String(string)
byte[] byteArray = Convert.FromBase64String(base64String);
Why though?
Images are "big" binary data. Base64 blows each byte up into hexadecimal chars which adds a lot of overhead.
=> Why not rather send the binary data directly?
See MultiPartFormFileSection and use the overload public static Networking.UnityWebRequest Post(string uri, List<IMultipartFormSection> multipartFormSections);
like e.g.
IEnumerator Upload(byte[] fileContent)
{
var sections = new List<IMultiPartFormSection>();
// According to your servers needs
sections.Add(new MultiPartFormFileSection("files", fileContent, "photo.png", "image/png"));
using(UnityWebRequest www = UnityWebRequest.Post("http://127.0.0.1:5000/", sections))
{
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
Debug.Log("Form upload complete!");
// Get the server response
// NO NEED FOR A GET REQUESTS!
var serverResponse = www.downloadHandler.text;
Debug.Log(serverResponse);
}
}
}
and for the downloading accordingly use UnityWebRequestTexture.GetTexture like
using (var uwr = UnityWebRequestTexture.GetTexture("https://www.my-server.com/myimage.png"))
{
yield return uwr.SendWebRequest();
if (uwr.result != UnityWebRequest.Result.Success)
{
Debug.Log(uwr.error);
}
else
{
// Get downloaded texture
var texture = DownloadHandlerTexture.GetContent(uwr);
...
}
}

Step Over throws exception, but Step Into not Unity

I am making a unity game and have one strange situation, which I will try to explain.
Here is my UserCreator class in which I want to return nativeCountry from another class (MySQLCountryManager):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class UserCreator : MonoBehaviour
{
public static PersonModel CreateUser(PersonModel model)
{
CountryModel nativeCountry = new CountryModel();
nativeCountry = MySQLCountryManager.GetCountryByName(model.NativeCountry);
<some other code here....>
}
}
And here is MySQLCountryManager class with GetCountryByName method:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
public class MySQLCountryManager : MonoBehaviour
{
public static CountryModel GetCountryByName(string countryName)
{
CountryModel country = new CountryModel();
WWWForm form = new WWWForm();
form.AddField("CountryName", countryName);
UnityWebRequestAsyncOperation getCountryByName = new UnityWebRequestAsyncOperation();
getCountryByName = UnityWebRequest.Post(WebReguests.getCountryByName, form).SendWebRequest();
List<string> results = new List<string>();
if (getCountryByName.webRequest.isNetworkError || getCountryByName.webRequest.isHttpError)
{
Debug.LogError(getCountryByName.webRequest.error);
}
else
{
var data = getCountryByName.webRequest.downloadHandler.text;
string[] result = data.Split(',');
for (int i = 0; i < result.Length; i++)
{
results.Add(result[i]);
}
int id;
bool idOk;
idOk = int.TryParse(results[0], out id );
if (idOk)
{
country.id = id;
}
country.CountryName = results[1];
byte[] flagBytes = Convert.FromBase64String(results[2]);
country.Flag = flagBytes;
byte[] shapeBytes = Convert.FromBase64String(results[3]);
country.Shape = shapeBytes;
byte[] woP = Convert.FromBase64String(results[4]);
country.WorldPosition = woP;
byte[] coa = Convert.FromBase64String(results[5]);
country.CoatOfArms = coa;
byte[] dishPicBytes = Convert.FromBase64String(results[6]);
country.DishPic = dishPicBytes;
byte[] curiosityBytes = Convert.FromBase64String(results[7]);
country.CuriosityPic = curiosityBytes;
country.Continent = results[8];
country.Population = results[9];
country.Capital = results[10];
country.Language = results[11];
country.Currency = results[12];
country.Religion = results[13];
country.DishName = results[14];
}
return country;
}
Now, the problem is, when I start debugging project in Visual Studio 2019 if I go Step Into on native country in UserCreator and then Step Into in MySQLCountryManager.GetCountryById, code works fine and returns nativeCountry as I expect. But when I go Step Over nativeCountry, it always throws some exceptions, like 'Index was out of range', 'Input string is not in a correct format' etc. And this is happening for all methods with UnityWebRequest called from UserCreator class.
I tried to google for this but nothing useful was found.
Any idea why this is happening?
tl;dr: Sounds like a race condition to me!
While stepping into the method you most probably give the asynchronous request just enough time to actually finish in the background.
I don't see where you are waiting for the results of the asynchronous download.
See the examples of UnityWebRequest.Post => as with any other asynchronous execution you have to wait until the results are actually back.
Usually you would use a Coroutine with callback like e.g.
public static void GetCountryByNameAsync(MonoBehaviour caller, string countryName, Action<CountryModel> whenDone)
{
caller.StartCoroutine(GetCountryByNameRoutine(countryName, whenDone))
}
private static IEnumerator GetCountryByNameRoutine(string countryName, Action<CountryModel> whenDone)
{
var form = new WWWForm();
form.AddField("CountryName", countryName);
using(var request = UnityWebRequest.Post(WebReguests.getCountryByName, form))
{
// WAIT until the request has either failed or succeeded
yield return request.SendWebRequest();
if (request.isNetworkError || request.isHttpError)
{
Debug.LogError(request.error);
yield break;
}
var data = request.downloadHandler.text;
var results = data.Split(',');
var country = new CountryModel();
if (int.TryParse(results[0], out var id))
{
country.id = id;
}
country.CountryName = results[1];
var flagBytes = Convert.FromBase64String(results[2]);
country.Flag = flagBytes;
var shapeBytes = Convert.FromBase64String(results[3]);
country.Shape = shapeBytes;
var woP = Convert.FromBase64String(results[4]);
country.WorldPosition = woP;
var coa = Convert.FromBase64String(results[5]);
country.CoatOfArms = coa;
var dishPicBytes = Convert.FromBase64String(results[6]);
country.DishPic = dishPicBytes;
var curiosityBytes = Convert.FromBase64String(results[7]);
country.CuriosityPic = curiosityBytes;
country.Continent = results[8];
country.Population = results[9];
country.Capital = results[10];
country.Language = results[11];
country.Currency = results[12];
country.Religion = results[13];
country.DishName = results[14];
whenDone?.Invoke(country);
}
}
then later you would rather use it like e.g.
// caller will be whatever script is actually calling this method
// we will take that reference to make it also responsible for executing the according Coroutine
public static void CreateUserAsync(MonoBehaviour caller, PersonModel model, Action<PersonModel> whenDone)
{
MySQLCountryManager.GetCountryByName(caller, model.NativeCountry, nativeCountry =>
{
<some other code here....>
whenDone?.Invoke(theCreatedPersonModel);
});
}
Or if it gets more complex instead of Coroutines you might rather use an async - await approach and pass the final result back into the Unity main thread.

Recording a video to file with ios

I'm trying to find a working sample to record videos with IOS (using xamarin) but there's always something missing or not working for me.
My best try using several forum posts and samples is the following :
using System;
using CoreGraphics;
using Foundation;
using UIKit;
using AVFoundation;
using CoreVideo;
using CoreMedia;
using CoreFoundation;
using System.IO;
using AssetsLibrary;
namespace avcaptureframes {
public partial class AppDelegate : UIApplicationDelegate {
public static UIImageView ImageView;
UIViewController vc;
AVCaptureSession session;
OutputRecorder outputRecorder;
DispatchQueue queue;
public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
{
ImageView = new UIImageView (new CGRect (10f, 10f, 200f, 200f));
ImageView.ContentMode = UIViewContentMode.Top;
vc = new UIViewController {
View = ImageView
};
window.RootViewController = vc;
window.MakeKeyAndVisible ();
window.BackgroundColor = UIColor.Black;
if (!SetupCaptureSession ())
window.AddSubview (new UILabel (new CGRect (20f, 20f, 200f, 60f)) {
Text = "No input device"
});
return true;
}
bool SetupCaptureSession ()
{
// configure the capture session for low resolution, change this if your code
// can cope with more data or volume
session = new AVCaptureSession {
SessionPreset = AVCaptureSession.PresetMedium
};
// create a device input and attach it to the session
var captureDevice = AVCaptureDevice.DefaultDeviceWithMediaType (AVMediaType.Video);
if (captureDevice == null) {
Console.WriteLine ("No captureDevice - this won't work on the simulator, try a physical device");
return false;
}
//Configure for 15 FPS. Note use of LockForConigfuration()/UnlockForConfiguration()
NSError error = null;
captureDevice.LockForConfiguration (out error);
if (error != null) {
Console.WriteLine (error);
captureDevice.UnlockForConfiguration ();
return false;
}
if (UIDevice.CurrentDevice.CheckSystemVersion (7, 0))
captureDevice.ActiveVideoMinFrameDuration = new CMTime (1, 15);
captureDevice.UnlockForConfiguration ();
var input = AVCaptureDeviceInput.FromDevice (captureDevice);
if (input == null) {
Console.WriteLine ("No input - this won't work on the simulator, try a physical device");
return false;
}
session.AddInput (input);
// create a VideoDataOutput and add it to the sesion
var settings = new CVPixelBufferAttributes {
PixelFormatType = CVPixelFormatType.CV32BGRA
};
using (var output = new AVCaptureVideoDataOutput { WeakVideoSettings = settings.Dictionary }) {
queue = new DispatchQueue ("myQueue");
outputRecorder = new OutputRecorder ();
output.SetSampleBufferDelegate (outputRecorder, queue);
session.AddOutput (output);
}
session.StartRunning ();
return true;
}
public override void OnActivated (UIApplication application)
{
}
public class OutputRecorder : AVCaptureVideoDataOutputSampleBufferDelegate
{
AVAssetWriter writer=null;
AVAssetWriterInput writerinput= null;
CMTime lastSampleTime;
int frame=0;
NSUrl url;
public OutputRecorder()
{
string tempFile = Path.Combine(Path.GetTempPath(), "NewVideo.mp4");
if (File.Exists(tempFile)) File.Delete(tempFile);
url = NSUrl.FromFilename(tempFile);
NSError assetWriterError;
writer = new AVAssetWriter(url, AVFileType.Mpeg4, out assetWriterError);
var outputSettings = new AVVideoSettingsCompressed()
{
Height = 300,
Width = 300,
Codec = AVVideoCodec.H264,
CodecSettings = new AVVideoCodecSettings
{
AverageBitRate = 1000000
}
};
writerinput = new AVAssetWriterInput(mediaType: AVMediaType.Video, outputSettings: outputSettings);
writerinput.ExpectsMediaDataInRealTime = false;
writer.AddInput(writerinput);
}
public override void DidOutputSampleBuffer (AVCaptureOutput captureOutput, CMSampleBuffer sampleBuffer, AVCaptureConnection connection)
{
try
{
lastSampleTime = sampleBuffer.PresentationTimeStamp;
var image = ImageFromSampleBuffer(sampleBuffer);
if (frame == 0)
{
writer.StartWriting();
writer.StartSessionAtSourceTime(lastSampleTime);
frame = 1;
}
String infoString = "";
if (writerinput.ReadyForMoreMediaData)
{
if (!writerinput.AppendSampleBuffer(sampleBuffer))
{
infoString = "Failed to append sample buffer";
}
else
{
infoString = String.Format("{0} frames captured", frame++);
}
}
else
{
infoString = "Writer not ready";
}
Console.WriteLine(infoString);
ImageView.BeginInvokeOnMainThread(() => ImageView.Image = image);
}
catch (Exception e)
{
Console.WriteLine(e);
}
finally
{
sampleBuffer.Dispose();
}
}
UIImage ImageFromSampleBuffer (CMSampleBuffer sampleBuffer)
{
// Get the CoreVideo image
using (var pixelBuffer = sampleBuffer.GetImageBuffer () as CVPixelBuffer)
{
// Lock the base address
pixelBuffer.Lock (CVOptionFlags.None);
// Get the number of bytes per row for the pixel buffer
var baseAddress = pixelBuffer.BaseAddress;
var bytesPerRow = (int)pixelBuffer.BytesPerRow;
var width = (int)pixelBuffer.Width;
var height = (int)pixelBuffer.Height;
var flags = CGBitmapFlags.PremultipliedFirst | CGBitmapFlags.ByteOrder32Little;
// Create a CGImage on the RGB colorspace from the configured parameter above
using (var cs = CGColorSpace.CreateDeviceRGB ())
{
using (var context = new CGBitmapContext (baseAddress, width, height, 8, bytesPerRow, cs, (CGImageAlphaInfo)flags))
{
using (CGImage cgImage = context.ToImage ())
{
pixelBuffer.Unlock (CVOptionFlags.None);
return UIImage.FromImage (cgImage);
}
}
}
}
}
void TryDispose (IDisposable obj)
{
if (obj != null)
obj.Dispose ();
}
}
}
}
This works displaying live camera image and I get "frames captured" message in consol but I don't find how to record to file.
I read somewhere about adding VideoCapture but I don't know how to link with my code.
Any help will is welcome.
From your code, in the construct of class OutputRecorder you have defined the url where you want to save the recording:
string tempFile = Path.Combine(Path.GetTempPath(), "NewVideo.mp4");
if (File.Exists(tempFile)) File.Delete(tempFile);
url = NSUrl.FromFilename(tempFile);
It means you want to save the video in the tmp folder in the app's sandbox. If you want to use the video sometime later, I recommend you to change the folder to documents by using:
string filePath = Path.Combine(NSSearchPath.GetDirectories(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User)[0], "NewVideo.mp4");
I notice that you have called session.StartRunning(); in the method bool SetupCaptureSession() to start recording. please add session.StopRunning(); to end recording then the video will be saved in the path we just defined above.
Moreover, you can retrieve the video with the path like:
NSData videoData = NSData.FromFile(filePath);

Delay in accessing recently added data via Web Server [duplicate]

I am creating a HoloLens app using Unity which has to take data from a REST API and display it.
I am currently using WWW datatype to get the data and yield return statement in a coroutine that will be called from the Update() function. When I try to run the code, I get the latest data from the API but when someone pushes any new data onto the API, it does not automatically get the latest data in real time and I have to restart the app to see the latest data.
My Code:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
public class TextChange : MonoBehaviour {
// Use this for initialization
WWW get;
public static string getreq;
Text text;
bool continueRequest = false;
void Start()
{
StartCoroutine(WaitForRequest());
text = GetComponent<Text>();
}
// Update is called once per frame
void Update()
{
}
private IEnumerator WaitForRequest()
{
if (continueRequest)
yield break;
continueRequest = true;
float requestFrequencyInSec = 5f; //Update after every 5 seconds
WaitForSeconds waitTime = new WaitForSeconds(requestFrequencyInSec);
while (continueRequest)
{
string url = "API Link goes Here";
WWW get = new WWW(url);
yield return get;
getreq = get.text;
//check for errors
if (get.error == null)
{
string json = #getreq;
List<MyJSC> data = JsonConvert.DeserializeObject<List<MyJSC>>(json);
int l = data.Count;
text.text = "Data: " + data[l - 1].content;
}
else
{
Debug.Log("Error!-> " + get.error);
}
yield return waitTime; //Wait for requestFrequencyInSec time
}
}
void stopRequest()
{
continueRequest = false;
}
}
public class MyJSC
{
public string _id;
public string author;
public string content;
public string _v;
public string date;
}
This is happening because resources caching is enabled on the Server.
Three possible solutions I know about:
1.Disable resources caching on the server. Instructions are different for every web server. Usually done in .htaccess.
2.Make each request with unique timestamp. The time should in Unix format.
This method will not work on iOS. You are fine since this is for HoloLens.
For example, if your url is http://url.com/file.rar, append ?t=currentTime at the end. currentTime is the actual time in Unix Format.
Full example url: http://url.com/file.rar?t=1468475141
Code:
string getUTCTime()
{
System.Int32 unixTimestamp = (System.Int32)(System.DateTime.UtcNow.Subtract(new System.DateTime(1970, 1, 1))).TotalSeconds;
return unixTimestamp.ToString();
}
private IEnumerator WaitForRequest()
{
string url = "API Link goes Here" + "?t=" + getUTCTime();
WWW get = new WWW(url);
yield return get;
getreq = get.text;
//check for errors
if (get.error == null)
{
string json = #getreq;
List<MyJSC> data = JsonConvert.DeserializeObject<List<MyJSC>>(json);
int l = data.Count;
text.text = "Data: " + data[l - 1].content;
}
else
{
Debug.Log("Error!-> " + get.error);
}
}
3.Disable Cache on the client side by supplying and modifying the Cache-Control and Pragma headers in the request.
Set Cache-Control header to max-age=0, no-cache, no-store then set Pragma header to no-cache.
I suggest you do this with UnityWebRequest instead of the WWW class. First, Include using UnityEngine.Networking;.
Code:
IEnumerator WaitForRequest(string url)
{
UnityWebRequest www = UnityWebRequest.Get(url);
www.SetRequestHeader("Cache-Control", "max-age=0, no-cache, no-store");
www.SetRequestHeader("Pragma", "no-cache");
yield return www.Send();
if (www.isError)
{
Debug.Log(www.error);
}
else
{
Debug.Log("Received " + www.downloadHandler.text);
}
}

Web API - Get progress when uploading to Azure storage

The task i want to accomplish is to create a Web API service in order to upload a file to Azure storage. At the same time, i would like to have a progress indicator that reflects the actual upload progress. After some research and studying i found out two important things:
First is that i have to split the file manually into chunks, and upload them asynchronously using the PutBlockAsync method from Microsoft.WindowsAzure.Storage.dll.
Second, is that i have to receive the file in my Web API service in Streamed mode and not in Buffered mode.
So until now i have the following implementation:
UploadController.cs
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using WebApiFileUploadToAzureStorage.Infrastructure;
using WebApiFileUploadToAzureStorage.Models;
namespace WebApiFileUploadToAzureStorage.Controllers
{
public class UploadController : ApiController
{
[HttpPost]
public async Task<HttpResponseMessage> UploadFile()
{
if (!Request.Content.IsMimeMultipartContent("form-data"))
{
return Request.CreateResponse(HttpStatusCode.UnsupportedMediaType,
new UploadStatus(null, false, "No form data found on request.", string.Empty, string.Empty));
}
var streamProvider = new MultipartAzureBlobStorageProvider(GetAzureStorageContainer());
var result = await Request.Content.ReadAsMultipartAsync(streamProvider);
if (result.FileData.Count < 1)
{
return Request.CreateResponse(HttpStatusCode.BadRequest,
new UploadStatus(null, false, "No files were uploaded.", string.Empty, string.Empty));
}
return Request.CreateResponse(HttpStatusCode.OK);
}
private static CloudBlobContainer GetAzureStorageContainer()
{
var storageConnectionString = ConfigurationManager.AppSettings["AzureBlobStorageConnectionString"];
var storageAccount = CloudStorageAccount.Parse(storageConnectionString);
var blobClient = storageAccount.CreateCloudBlobClient();
blobClient.DefaultRequestOptions.SingleBlobUploadThresholdInBytes = 1024 * 1024;
var container = blobClient.GetContainerReference("photos");
if (container.Exists())
{
return container;
}
container.Create();
container.SetPermissions(new BlobContainerPermissions
{
PublicAccess = BlobContainerPublicAccessType.Container
});
return container;
}
}
}
MultipartAzureBlobStorageProvider.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage.Blob;
namespace WebApiFileUploadToAzureStorage.Infrastructure
{
public class MultipartAzureBlobStorageProvider : MultipartFormDataStreamProvider
{
private readonly CloudBlobContainer _blobContainer;
public MultipartAzureBlobStorageProvider(CloudBlobContainer blobContainer) : base(Path.GetTempPath())
{
_blobContainer = blobContainer;
}
public override Task ExecutePostProcessingAsync()
{
const int blockSize = 256 * 1024;
var fileData = FileData.First();
var fileName = Path.GetFileName(fileData.Headers.ContentDisposition.FileName.Trim('"'));
var blob = _blobContainer.GetBlockBlobReference(fileName);
var bytesToUpload = (new FileInfo(fileData.LocalFileName)).Length;
var fileSize = bytesToUpload;
blob.Properties.ContentType = fileData.Headers.ContentType.MediaType;
blob.StreamWriteSizeInBytes = blockSize;
if (bytesToUpload < blockSize)
{
var cancellationToken = new CancellationToken();
using (var fileStream = new FileStream(fileData.LocalFileName, FileMode.Open, FileAccess.ReadWrite))
{
var upload = blob.UploadFromStreamAsync(fileStream, cancellationToken);
Debug.WriteLine($"Status {upload.Status}.");
upload.ContinueWith(task =>
{
Debug.WriteLine($"Status {task.Status}.");
Debug.WriteLine("Upload is over successfully.");
}, TaskContinuationOptions.OnlyOnRanToCompletion);
upload.ContinueWith(task =>
{
Debug.WriteLine($"Status {task.Status}.");
if (task.Exception != null)
{
Debug.WriteLine("Task could not be completed." + task.Exception.InnerException);
}
}, TaskContinuationOptions.OnlyOnFaulted);
upload.Wait(cancellationToken);
}
}
else
{
var blockIds = new List<string>();
var index = 1;
long startPosition = 0;
long bytesUploaded = 0;
do
{
var bytesToRead = Math.Min(blockSize, bytesToUpload);
var blobContents = new byte[bytesToRead];
using (var fileStream = new FileStream(fileData.LocalFileName, FileMode.Open))
{
fileStream.Position = startPosition;
fileStream.Read(blobContents, 0, (int)bytesToRead);
}
var manualResetEvent = new ManualResetEvent(false);
var blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(index.ToString("d6")));
Debug.WriteLine($"Now uploading block # {index.ToString("d6")}");
blockIds.Add(blockId);
var upload = blob.PutBlockAsync(blockId, new MemoryStream(blobContents), null);
upload.ContinueWith(task =>
{
bytesUploaded += bytesToRead;
bytesToUpload -= bytesToRead;
startPosition += bytesToRead;
index++;
var percentComplete = (double)bytesUploaded / fileSize;
Debug.WriteLine($"Percent complete: {percentComplete.ToString("P")}");
manualResetEvent.Set();
});
manualResetEvent.WaitOne();
} while (bytesToUpload > 0);
Debug.WriteLine("Now committing block list.");
var putBlockList = blob.PutBlockListAsync(blockIds);
putBlockList.ContinueWith(task =>
{
Debug.WriteLine("Blob uploaded completely.");
});
putBlockList.Wait();
}
File.Delete(fileData.LocalFileName);
return base.ExecutePostProcessingAsync();
}
}
}
I also enabled Streamed mode as this blog post suggests. This approach works great, in terms that the file is uploaded successfully to Azure storage. Then, when i make a call to this service making use of XMLHttpRequest (and subscribing to the progress event) i see the indicator moving to 100% very quickly. If a 5MB file needs around 1 minute to upload, my indicator moves to the end in just 1 second. So probably the problem resides in the way that the server informs the client about the upload progress. Any thoughts about this? Thank you.
================================ Update 1 ===================================
That is the JavaScript code i use to call the service
function uploadFile(file, index, uploadCompleted) {
var authData = localStorageService.get("authorizationData");
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", function (event) {
fileUploadPercent = Math.floor((event.loaded / event.total) * 100);
console.log(fileUploadPercent + " %");
});
xhr.onreadystatechange = function (event) {
if (event.target.readyState === event.target.DONE) {
if (event.target.status !== 200) {
} else {
var parsedResponse = JSON.parse(event.target.response);
uploadCompleted(parsedResponse);
}
}
};
xhr.open("post", uploadFileServiceUrl, true);
xhr.setRequestHeader("Authorization", "Bearer " + authData.token);
var data = new FormData();
data.append("file-" + index, file);
xhr.send(data);
}
your progress indicator might be moving rapidly fast, might be because of
public async Task<HttpResponseMessage> UploadFile()
i have encountered this before, when creating an api of async type, im not even sure if it can be awaited, it will just of course just finish your api call on the background, reason your progress indicator instantly finish, because of the async method (fire and forget). the api will immediately give you a response, but will actually finish on the server background (if not awaited).
please kindly try making it just
public HttpResponseMessage UploadFile()
and also try these ones
var result = Request.Content.ReadAsMultipartAsync(streamProvider).Result;
var upload = blob.UploadFromStreamAsync(fileStream, cancellationToken).Result;
OR
var upload = await blob.UploadFromStreamAsync(fileStream, cancellationToken);
hope it helps.
Other way to acomplish what you want (I don't understand how the XMLHttpRequest's progress event works) is using the ProgressMessageHandler to get the upload progress in the request. Then, in order to notify the client, you could use some cache to store the progress, and from the client request the current state in other endpoint, or use SignalR to send the progress from the server to the client
Something like:
//WebApiConfigRegister
var progress = new ProgressMessageHandler();
progress.HttpSendProgress += HttpSendProgress;
config.MessageHandlers.Add(progress);
//End WebApiConfig Register
private static void HttpSendProgress(object sender, HttpProgressEventArgs e)
{
var request = sender as HttpRequestMessage;
//todo: check if request is not null
//Get an Id from the client or something like this to identify the request
var id = request.RequestUri.Query[0];
var perc = e.ProgressPercentage;
var b = e.TotalBytes;
var bt = e.BytesTransferred;
Cache.InsertOrUpdate(id, perc);
}
You can check more documentation on this MSDN blog post (Scroll down to "Progress Notifications" section)
Also, you could calculate the progress based on the data chunks, store the progress in a cache, and notify in the same way as above. Something like this solution

Categories