Django example on GitHub
A Django 6 app with no database.
Quickstart
1
Create the uploads app
These paths assume a project made with
django-admin startproject config .. Create an app for the upload code and register it, so Django finds its templates and static files.config/settings.py
2
Add your library credentials
.env
.env on its own, and uv run --env-file .env manage.py runserver loads it for you.3
Create the Bunny Stream module
urllib covers the two API calls. The upload signature is a SHA-256 of the library ID, API key, expiry, and video ID.uploads/bunny_stream.py
4
Add the views
index renders the page. create_upload re-signs an unfinished video when the browser sends its ID, and creates a new one otherwise.uploads/views.py
startproject created.config/urls.py
5
Upload from the browser
Render the CSRF token into the page. The CSRF middleware rejects the There’s no build step. tus-js-client comes from jsDelivr, and the token travels as tus-js-client sends the credentials as headers with every request. The video ID goes into
POST without it.uploads/templates/uploads/index.html
X-CSRFToken.uploads/static/uploads/video-uploader.js
localStorage against the file, which is how a reload finds its way back to the same upload. Add this to the same file.abort() pauses. start() picks up from the last chunk we acknowledged.A 401 from the TUS endpoint means the signature doesn’t match the headers. Check that the library ID and API key belong to the same library. A 400 means the expiry has already passed. Re-signing keeps the upload’s original expiry, as the TUS FAQ explains.Play it once it’s encoded
We start encoding when the last chunk arrives. Poll your status route untilstatus reaches 4 (finished), 5 or 6 (failed), then embed embedUrl. This goes in the same file too.
encodeProgress gives you a percentage to show in the meantime. A webhook tells your server when encoding finishes.
Then wire both to the page’s #video-file input and #video-output element. Picking another file cancels the current upload.
uv run --env-file .env manage.py runserver, open http://localhost:8000, and choose a video.
Before you deploy
Wrap both views inlogin_required and record who owns each video ID. The example’s settings.py is for development, and production needs DJANGO_SECRET_KEY, DEBUG = False, and ALLOWED_HOSTS.
Troubleshooting
/api/uploads returns 403 with CSRF verification failed
/api/uploads returns 403 with CSRF verification failed
The template needs the
csrf-token meta tag, and the fetch needs the X-CSRFToken header.