Direct Creator Uploads

Using Direct Creator Uploads with TUS #

This is a working draft but needs some polish before being added to official docs.

Use Direct Creator Uploads for a user to send a video to Stream directly on your behalf. If their uploads will be 200mb or more, or they need support for resumable uploads, use this in combination with TUS.

Canonical Documentation:

At a high level, the implementation is two parts, like with a standard direct upload:

  • A Worker or server-side / origin application makes the initial, authenticated API request to Stream to provision the upload URL. But enabling it for TUS.
  • The end-user side performs an unauthenticated upload directly to that endpoint.
  sequenceDiagram

  participant User
  participant YourServer
  participant Stream

  User -->> User: Wants to upload a video
  User -->> YourServer: Send upload intent and file size
  YourServer -->> Stream: Create Direct Creator TUS Upload
  Stream -->> YourServer: Return TUS URL
  YourServer -->> User: Return TUS URL
  User -->> Stream: TUS Upload file to TUS URL

Summary Code Samples #

Your Worker or Server Side #

Once you know your user wants to do an upload, and you have the filesize of the file they want to upload, your backend or a work Worker makes an authenticated API call to Stream to provision the upload endpoint.

Here’s an example:

// @TODO: Your user needs to pass the filesize to you.
const size = 123;

// THIS PART RUNS ON THE SERVER

const provisionEndpoint =
  "https://api.cloudflare.com/client/v4/accounts/ACCOUNT_TAG/stream?direct_user=true";
const options = {
  method: 'POST',
  headers: {
    Authorization: "Bearer MY_API_KEY",
    "Upload-Creator": "creator_id_if_applicable",
    "Tus-Resumable": "1.0.0",
    "Upload-Length": size,
    "Upload-Metadata":
      `maxdurationseconds ${btoa('600')}, name ${btoa('new upload')}`
  },
};

// Provision the Direct Creator Upload URL with Stream:
const res = await fetch(provisionEndpoint, options);

if (!res.ok) {
  // @TODO: Catch request failures
}

// This is the endpoint to send back to the customer.
const uploadEndpoint = res.headers.get('Location');

In short, make a POST request:

  • to /client/v4/accounts/ACCOUNT_TAG/stream?direct_user=true
  • with the "Tus-Resumable": "1.0.0" and Upload-Length headers

On success, this returns an HTTP 201 response with a Location header, which the end-user can run a TUS upload to.

End-user / Creator Side #

import * as fs from "fs";
import * as tus from "tus-js-client";

// @TODO: You likely have a file reference from however you determined the size
var path = "/mnt/c/Users/TaylorSmith/Desktop/austin-mobile.mp4";
var file = fs.createReadStream(path);

const uploadEndpoint = '<PLACEHOLDER_FROM_SERVER>';

// THIS PART RUNS ON THE CLIENT
const uploadOptions = {
  endpoint: uploadEndpoint,
    // ^ This is the `Location` header from above
  chunkSize: 50 * 1024 * 1024,
    // ^ Required a minimum chunk size of 5 MB. Here we use 50 MB.
  retryDelays: [0, 3000, 5000, 10000, 20000],
  onError: function (error) {
    throw error;
  },
  onProgress: function (bytesUploaded, bytesTotal) {
    const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
    // @TODO: Update a progress bar or text field
  },
  onSuccess: function () {
    console.log("Upload finished");
    // @TODO: Indicate to user that the upload is finished
  },
  onAfterResponse: function (req, res) {
    // Here's one way to capture the video ID as it is being uploaded.
    return new Promise((resolve) => {
      const mediaIdHeader = res.getHeader("stream-media-id");
      if (mediaIdHeader) {
        mediaId = mediaIdHeader;
      }
      resolve();
    });
  },
};

const upload = new tus.Upload(file, uploadOptions);
upload.start();

This example uses tus-js-client in a simple Node.js script. There are other TUS implementations in other languages that can be used as well.

A Working Example #

This reference implementation shows how to make a Cloudflare API request to get a creator upload URL for TUS. This is a demonsration only. Do not implement this browser-side because that would expose your API credentials.

1. User wants to upload a file #

In your application, your user intends to upload a file. They’ll pick a file:

In your application’s call to your API/backend, include the file size.

2. Your server/Worker makes a TUS Upload URL #

File Size

The initial request to get an upload URL requires a file size, but not the actual file.

Account ID/Tag
Cloudflare API Key
Paste your API key here. It will be submitted to Cloudflare directly, not sent to me. However, never ever provide your API to client-side code, this is a functional example for reference only.
Options

Check the list of supported options for Upload-Metadata. For this example, a few will be hardcoded.

  • Maximum Duration: 1 hour
  • Name: Example Upload
Submit
Response

On success, this request will return an HTTP 201 response with a `Location` header. Your application should send this URL to your end-user's client. This will be their upload destination.


<script>
  fileEl = document.getElementById('file');
  bytesEl = document.getElementById('bytes');
  acctEl = document.getElementById('acct');
  keyEl = document.getElementById('key');
  responseEl = document.getElementById('location');

  fileEl.addEventListener('change', e => {
    console.log('running')
    bytesEl.value = e.target.files[0].size;
  });

  document.getElementById('getTusDCUpload')?.addEventListener('click', async (e) => {
    e.preventDefault();

    const provisionEndpoint =
      `https://api.cloudflare.com/client/v4/accounts/${acctEl.value}/stream?direct_user=true`;
    const options = {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${keyEl.value}`,
        "Tus-Resumable": "1.0.0",
        "Upload-Length": bytesEl.value,
        "Upload-Metadata":
          `maxdurationseconds ${btoa('3600')}, name ${btoa('Example Upload')}`,
      },
    };

    // Provision the Direct Creator Upload URL with Stream:
    const res = await fetch(provisionEndpoint, options);

    if (!res.ok) {
      responseEl.value = (`Request failed ${res.status}\n`);
      return;
    }

    uploadEndpoint = res.headers.get('Location');

    console.log(`End user should upload to: ${uploadEndpoint}`);
    responseEl.value = uploadEndpoint;

    // Populate the part 2 fields.
    document.getElementById('pt2filename').value = `${fileEl.files[0].name}`;
    document.getElementById('pt2location').value = `${uploadEndpoint}`;
  });

</script>

3. Your End-user Uploads Directly #

In your response to the end-user, include that Location header value so they can upload the file to it directly. In this demo, we’ve selected the file already:

Established Inputs

Direct Upload
Progress and Updates

tus-js-client does not provide a UI, but does throw many events. Uppy offers some prebuilt UI tools.


<script src="https://cdn.jsdelivr.net/npm/tus-js-client@latest/dist/tus.min.js"></script>

<script>
  const pt2locationEl = document.getElementById('pt2location');
  const pt2progressEl = document.getElementById('pt2progress');

  document.getElementById('doTusUpload').addEventListener('click', async (e) => {
    e.preventDefault();

    // For determining the video id.
    let mediaId = false;

    const uploadOptions = {
      endpoint: pt2locationEl.value,
      // ^ This is the `Location` header from above
      chunkSize: 50 * 1024 * 1024,
      // ^ Required a minimum chunk size of 5 MB.
      retryDelays: [0, 3000, 5000, 10000, 20000],
      // ^ Delays after which it will retry if the upload fails.
      onError: function (error) {
        pt2progressEl.value += `\n\nError: ${error}`;
        throw error;
      },
      onProgress: function (bytesUploaded, bytesTotal) {
        const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
        pt2progressEl.value += `\n\nUpload Progress: ${percentage}`;
      },
      onSuccess: function () {
        pt2progressEl.value += `\n\nUpload completed.`;
        pt2progressEl.value += `\n\nVideo ID: ${mediaId}.`;
      },

      // One way to get the video ID from one of the TUS requests.
      onAfterResponse: function (req, res) {
        return new Promise((resolve) => {
          var mediaIdHeader = res.getHeader("stream-media-id");
          if (mediaIdHeader) {
            mediaId = mediaIdHeader;
          }
          resolve();
        });
      },
    };

    const upload = new tus.Upload(fileEl.files[0], uploadOptions);
    upload.start();
  });
</script>