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:
- https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/
- https://developers.cloudflare.com/stream/uploading-videos/resumable-uploads/
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"andUpload-Lengthheaders
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.
|
| 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 |
|
<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>