Video Chunked Upload in Javascript

2 minute read

A scalable file uploading solution requires a chunk upload strategy, since large files cannot be held in memory at some point. I’ve been working on a project that requires uploading video files directly to Facebook servers without the backend being involved.

Here is the link fb-video-uploader to the module I created, which has no dependencies except babel-runtime to support async/await.

There are three stages of upload.

  • start - request with file size -> returns the chunk’s start/end offset and the upload_session_id

    Request Headers

      :authority: graph-video.facebook.com
      :method: POST
      :path: /v2.11/<ad account id>/advideos
      :scheme: https
      accept: application/json
      accept-encoding: gzip, deflate, br
      accept-language: en-US,en;q=0.9,bs;q=0.8
      content-length: 533
      content-type: multipart/form-data; boundary=----WebKitFormBoundaryAuH710A4wAnvEAnL
      origin: http://localhost:3000
      referer: http://localhost:3000/
      user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36	
    

    Request Payload

      ------WebKitFormBoundaryAuH710A4wAnvEAnL
      Content-Disposition: form-data; name="access_token"
    	
      <access token>
      ------WebKitFormBoundaryAuH710A4wAnvEAnL
      Content-Disposition: form-data; name="upload_phase"
    	
      start
      ------WebKitFormBoundaryAuH710A4wAnvEAnL
      Content-Disposition: form-data; name="file_size"
    	
      369763
      ------WebKitFormBoundaryAuH710A4wAnvEAnL--
    

    Response

      {
        upload_session_id: "10157823606217837",
        start_offset: "0",
        end_offset: "369763",
        video_id: "125123125125"
      }
    
  • transfer - request with upload_session_id, starting index and file blob of chunk size -> returns the next chunk’s start/end offset

    Request Headers

      :authority: graph-video.facebook.com
      :method: POST
      :path: /v2.11/<ad account id>/advideos
      :scheme: https
      accept: application/json
      accept-encoding: gzip, deflate, br
      accept-language: en-US,en;q=0.9,bs;q=0.8
      content-length: 370578
      content-type: multipart/form-data; boundary=----WebKitFormBoundaryjWtlmACzRHxI1JLV
      origin: http://localhost:3000
      referer: http://localhost:3000/
      user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36
    

    Request Payload

      ------WebKitFormBoundaryjWtlmACzRHxI1JLV
      Content-Disposition: form-data; name="access_token"
    	
      <access token>
      ------WebKitFormBoundaryjWtlmACzRHxI1JLV
      Content-Disposition: form-data; name="upload_phase"
    	
      transfer
      ------WebKitFormBoundaryjWtlmACzRHxI1JLV
      Content-Disposition: form-data; name="upload_session_id"
    	
      10157823606217837
      ------WebKitFormBoundaryjWtlmACzRHxI1JLV
      Content-Disposition: form-data; name="start_offset"
    	
      0
      ------WebKitFormBoundaryjWtlmACzRHxI1JLV
      Content-Disposition: form-data; name="video_file_chunk"; filename="blob"
      Content-Type: application/octet-stream
    	
    	
      ------WebKitFormBoundaryjWtlmACzRHxI1JLV--
    

    Response

      {
        start_offset: "369763",
        end_offset: "369763"
      }
    
  • finish - request with upload_session_id and video title -> returns completed status

    Request Headers

      :authority: graph-video.facebook.com
      :method: POST
      :path: /v2.11/<ad account id>/advideos
      :scheme: https
      accept: application/json
      accept-encoding: gzip, deflate, br
      accept-language: en-US,en;q=0.9,bs;q=0.8
      content-length: 553
      content-type: multipart/form-data; boundary=----WebKitFormBoundary0tqCdIckXVm1JTyG
      origin: http://localhost:3000
      referer: http://localhost:3000/
      user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36
    

    Request Payload

      ------WebKitFormBoundary0tqCdIckXVm1JTyG
      Content-Disposition: form-data; name="access_token"
    	
      <access token>
      ------WebKitFormBoundary0tqCdIckXVm1JTyG
      Content-Disposition: form-data; name="upload_phase"
    	
      finish
      ------WebKitFormBoundary0tqCdIckXVm1JTyG
      Content-Disposition: form-data; name="upload_session_id"
    	
      10157823606217837
      ------WebKitFormBoundary0tqCdIckXVm1JTyG--
    

    Response

      {
        success: true
      }
    

Notes

Requests are made to

https://graph-video.facebook.com/v2.11/<ad account id>/advideos

Using the the following content type (this is automatically set by XMLHttpRequest.

content-type: multipart/form-data

Get the File object from user input.

<input type="file" onchange="handleInputChange">

Get the file size.

file.size

Get the chunks using the File.slice method.

file.slice(start_offset, end_offset + 1);

By using the FormData to include the necessary data required for the requests, videos can uploaded easily as a serverless solution.

Certain browsers do not support FormData.set/delete methods, so create a new instance of FormData for each request and use the append method.

const formData = new FormData();
formData.append(key, value);

Don’t forget to include the access_token in the requests.