3

I'm building a site with wasm where users can post some text and attached pic and video to their post. The videos are posted to vimeo (it's like YouTube) using their APIs which is basically a web form with input element of type file and a submit button which POSTs the file to their servers directly from the client. Some videos can be large and thus posting that much data to vimeo server can take time.

What would be nice if the users are able to choose a file, click the submit button and continue writing their post while the video is being uploaded in the background instead of waiting for the posting of the video to finish. Also, it would nice if it was fail-safe where if the user finished writing his or her post sooner than the file upload, by navigating away, it won't halt the video posting process.

Please note that vimeo has other means of posting a video to their servers (for example pulling method). But all of them involves the videos landing on my servers first before uploading to their servers. That's costly in terms of bandwidth and storage.

Any suggestions? Can jquery help here?

Zuzlx
  • 1,246
  • 14
  • 33

1 Answers1

3

The solution is a little bit longer and has quite a few moving parts.

I'd recommend implementing the fail-safe by an independent service. This service is consumed by your CreatePost component and an UploadManager component. Be aware that this solution is not perfect. The entire "canceling upload" and error handling parts are missing. Also, there is no queue implemented.

Adding progress to uploads

The default HttpContent has no ability to be informed about an upload's progress. However, we can create a new implementation and add this functionality. I decided to use events for communications. The classes are copied and modified from this post.

This class FileTransferInforepresents the state of a transfer while FileTransferingProgressChangedEventArgs to copy the change any part of the application that might be interested in.

public class FileTransferingProgressChangedEventArgs : EventArgs
{
    public String Filename { get; set; }
    public Int64 BytesSent { get; init; }
    public Int64 TotalBytes { get; init; }
    public FileTransferInfo.States State { get; init; }
}

public class FileTransferInfo
{
    public enum States
    {
        Init,
        Pending,
        Transfering,
        PendingResponse,
        Finished
    }

    public States State { get; private set; }
    public Int64 BytesSent { get; private set; }
    public String Filename { get; private set; }
    public Int64 TotalSize { get; private set; }

    public FileTransferInfo(String filename, Int64 totalSize)
    {
        Filename = filename;
        TotalSize = totalSize;
        BytesSent = 0;
        State = States.Init;
    }

    private void SendEventHandler() => Progress?.Invoke(this,
        new FileTransferingProgressChangedEventArgs
        {
            BytesSent = BytesSent,
            State = State,
            TotalBytes = TotalSize,
            Filename = Filename
        });

    public event EventHandler<FileTransferingProgressChangedEventArgs> Progress;

    public void Start()
    {
        State = States.Transfering;
        SendEventHandler();
    }

    public void UpdateProgress(int length)
    {
        State = States.Transfering;
        BytesSent += length;
        SendEventHandler();
    }

    internal void UploadFinished()
    {
        State = States.PendingResponse;
        //just in case
        BytesSent = TotalSize;
        SendEventHandler();
    }

    internal void ResponseReceived()
    {
        State = States.Finished;
        SendEventHandler();
    }
}

This class is used by the ProgressableStreamContent which will be used by the HttpClient as a specialized way to upload files.

//copied from https://stackoverflow.com/questions/35320238/how-to-display-upload-progress-using-c-sharp-httpclient-postasync
public class ProgressableStreamContent : HttpContent
{
    private const int defaultBufferSize = 4096;

    private readonly Stream _content;
    private readonly Int32 _bufferSize;
    private Boolean _contentConsumed;
    private readonly FileTransferInfo _progressInfo;

    public ProgressableStreamContent(Stream content, FileTransferInfo progressInfo) : this(content, defaultBufferSize, progressInfo) { }

    public ProgressableStreamContent(Stream content, Int32 bufferSize, FileTransferInfo progressInfo)
    {
        if (bufferSize <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(bufferSize));
        }

        this._content = content ?? throw new ArgumentNullException(nameof(content));
        this._bufferSize = bufferSize;
        this._progressInfo = progressInfo;
    }

    protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        PrepareContent();

        var buffer = new Byte[this._bufferSize];

        _progressInfo.Start();

        using (_content)
        {
            while (true)
            {
                var length = await _content.ReadAsync(buffer, 0, buffer.Length);
                if (length <= 0) break;

                _progressInfo.UpdateProgress(length);

                stream.Write(buffer, 0, length);
            }
        }

        _progressInfo.UploadFinished();
    }

    protected override bool TryComputeLength(out long length)
    {
        length = _content.Length;
        return true;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _content.Dispose();
        }
        base.Dispose(disposing);
    }


    private void PrepareContent()
    {
        if (_contentConsumed)
        {
            // If the content needs to be written to a target stream a 2nd time, then the stream must support
            // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target 
            // stream (e.g. a NetworkStream).
            if (_content.CanSeek)
            {
                _content.Position = 0;
            }
            else
            {
                throw new InvalidOperationException("SR.net_http_content_stream_already_read");
            }
        }

        _contentConsumed = true;
    }
}

Upload Manager

The upload manager can initialize uploads and get the FileTransferInfo of ongoing/finished uploads.

public class UploadManager
{
    private readonly HttpClient _vimeoApiClient;
    private List<FileTransferInfo> _transfers = new();

    public IReadOnlyList<FileTransferInfo> Transfers => _transfers.AsReadOnly(); 

    public UploadManager(HttpClient vimeoApiClient)
    {
        _vimeoApiClient = vimeoApiClient;
    }

    public void StartFileUpload(Stream stream, String fileName, Int64 fileSize)
    {
        FileTransferInfo uploaderInfo = new FileTransferInfo(fileName, fileSize);
        uploaderInfo.Progress += UpdateFileProgressChanged;
        _transfers.Add(uploaderInfo);

        var singleFileContent = new ProgressableStreamContent(stream, uploaderInfo);
        //read the docs if vimeo expected such an encoded content
        var multipleFileContent = new MultipartFormDataContent();
        multipleFileContent.Add(singleFileContent, "file", fileName);

        Task.Run(async () =>
        {
            var result = await _vimeoApiClient.PostAsync("<YourURLHere>", multipleFileContent);
            uploaderInfo.ResponseReceived();

            uploaderInfo.Progress -= UpdateFileProgressChanged;
        });
    }

    private void UpdateFileProgressChanged(Object sender, FileTransferingProgressChangedEventArgs args) => FileTransferChanged?.Invoke(sender, args);

    public event EventHandler<FileTransferingProgressChangedEventArgs> FileTransferChanged;
}

The actual upload logic may be changed based on the needs of the API. This example shows how to use it with MultipartForm, where you can add more files or other content.

The event FileTransferChanged could be much finer, if needed. For now, it's just like a flow-type heater.

The upload manager needs to be registered for DI.

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    ....
    builder.Services.AddSingleton( sp => new UploadManager(
        new HttpClient { 
                BaseAddress = new Uri("<BaseURL>") } 
        ));
    await builder.Build().RunAsync();
}

The most improvements can be made here. The upload manager is the place for queueing and canceling.

Upload View The upload view is pretty straight-forward and can easily be tailored to your needs.

@inject UploadManager UploadManager
@implements IDisposable


<h3>Uploads</h3>
<ul>
    @foreach (var item in UploadManager.Transfers)
    {
        <li><span>@item.Filename</span> @(Math.Round( (100.0 * item.BytesSent / item.TotalSize),2 )) %</li>
    }
</ul>

@code {

    protected override void OnInitialized()
    {
        base.OnInitialized();
        UploadManager.FileTransferChanged += TransferProgressChanged;
    }

    private async void TransferProgressChanged(Object sender, FileTransferingProgressChangedEventArgs args)
    {
        await InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        UploadManager.FileTransferChanged -= TransferProgressChanged;
    }
}

** The NewPost component ```

With the UploadManager as the "heavy lifter," the file upload becomes easy.

@page "/NewPost"
@inject UploadManager UploadManager

<EditForm Model="_model">
    <InputText @bind-Value="_model.Name" class="form-control" />
    <InputFile OnChange="UploadFile" />
</EditForm>

<UploadManagerView />

@code {

    private void UploadFile(InputFileChangeEventArgs args)
    {
        // to make the demo easier, we assume that only one file is uploaded at a time
        if (args.FileCount > 1) { return; }

        UploadManager.StartFileUpload(args.File.OpenReadStream(/*SetMaxFileSizeHere*/), args.File.Name, args.File.Size);
    }

}

Just the benno
  • 2,306
  • 8
  • 12
  • Thank you for your detailed answer. I will try it out. – Zuzlx Jan 18 '21 at 23:00
  • @Zuzlx did it worked? Any suggestions for improvements? – Just the benno Jan 21 '21 at 05:34
  • Apologies. I've been swapped with my day job. I will look at it this weekend. – Zuzlx Jan 22 '21 at 13:10
  • HI Benno, I had a chance to try it out. I created a new project and copied your code snippets. I hope I didn't miss anything. Anyway, for some reason `UploadManager.StartFileUpload(args.File.OpenReadStream(5000000), args.File.Name, args.File.Size);` is not being called. I post the project on the github if you want to take a look. https://github.com/i12code/BennoSolution Thanks Benno for your help and insight. – Zuzlx Jan 23 '21 at 18:39
  • I've created an [issue](https://github.com/i12code/BennoSolution/issues/1) in the repo. Maybe we continue the discussion there? – Just the benno Jan 24 '21 at 07:35
  • Awesome solution. Well played! – Zuzlx Jan 24 '21 at 13:57
  • Thank you. It was my pleasure. A very interesting problem. :) – Just the benno Jan 25 '21 at 05:34
  • @Justthebenno Would it be possible to put this repo back online, I am interested in the solution – Musaffar Patel Nov 23 '22 at 19:31
  • 1
    @MusaffarPatel i got it working by the code posted here. – David Bouška Feb 15 '23 at 05:14