Bunny Stream video integration for SilverStripe — upload, manage, and embed videos straight from the CMS, with the video files hosted on Bunny Stream's CDN instead of your own server.
Developed and maintained by Restruct.
This is a fork of restruct/silverstripe-bunnystream.
Changes in this fork:
- Attach existing videos —
BunnyUploadFieldcan select an already-uploadedBunnyVideoinstead of only uploading a new one, with Bewerken/Ontkoppelen actions on the attached-video preview. - Upload-first video admin — the Video's admin "add new" button opens an
upload-only screen (just the upload control, no attach-existing picker). The upload
creates the
BunnyVideodirectly and redirects to its edit form, so no empty records are left behind. - Config-driven embed presets — templates render with
$Video/$Video.Embed('<preset>')from named YAML presets (see Embed presets), replacing hand-written per-consumer embed methods. - CMS title sync — renaming a video in the admin pushes the new title back to the Bunny library.
- Upload-field styling — Bootstrap-aligned CSS for the CMS upload control.
- Direct-to-Bunny uploads from the CMS — the browser uploads straight to Bunny via resumable TUS chunks, so large video files never pass through your web server.
- Re-use existing videos — attach a video that's already been uploaded instead of uploading it again.
BunnyVideoDataObject — a lightweight local record holding the Bunny GUID plus metadata (title, status, duration, dimensions, size) synced from the API.- Video admin — a
ModelAdminlisting all videos with thumbnails and status. - Embed helpers — ready-to-use player iframe HTML with per-video options (autoplay, mute, loop, controls, remember-position, start-time, enforce-full-watch).
- Signed embeds & uploads — optional token authentication for private libraries.
- Fail-closed deletes — deleting a
BunnyVideodeletes the remote video too, and won't silently orphan it if the API call fails.
- PHP 8.2+
- SilverStripe
^5 guzzlehttp/guzzle ^7.3- A Bunny.net account with a Stream library
This is a fork of restruct/silverstripe-bunnystream, not a Packagist original, so
it must be installed via a VCS repository entry.
In your project's root composer.json:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/hamaka/silverstripe-bunnystream"
}
]
}composer require restruct/silverstripe-bunnystream:dev-main-hmkComposer resolves
restruct/silverstripe-bunnystreamfrom your fork instead of Packagist because the VCS repository takes priority for that package name.
Build the database and expose the client assets:
vendor/bin/sake dev/build flush=1
composer vendor-exposecomposer show restruct/silverstripe-bunnystreamConfirm the source points to your fork's URL, not the upstream repository.
Set the following environment variables (e.g. in your .env):
| Variable | Required | Description |
|---|---|---|
BUNNY_STREAM_LIBRARY_ID |
Yes | Your Bunny Stream library ID. |
BUNNY_STREAM_API_KEY |
Yes | The library's API key (Bunny dashboard → Stream → your library → API). |
BUNNY_STREAM_CDN_HOSTNAME |
No | Custom pull-zone hostname for thumbnails (e.g. vz-xxxx.b-cdn.net). |
BUNNY_STREAM_TOKEN_AUTH_KEY |
No | Library "Token Authentication Key". When set and token auth is enabled on the library, embed URLs are signed and expire after a time window (default 4h). |
BUNNY_STREAM_LIBRARY_ID="12345"
BUNNY_STREAM_API_KEY="xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx"Add a has_one relation to BunnyVideo on any DataObject or page, and edit it
with BunnyUploadField:
use Restruct\BunnyStream\Forms\BunnyUploadField;
use Restruct\BunnyStream\Model\BunnyVideo;
use SilverStripe\Forms\FieldList;
class HomePage extends \Page
{
private static $has_one = [
'HeaderVideo' => BunnyVideo::class,
];
// Publish the linked video together with the page
private static $owns = [
'HeaderVideo',
];
public function getCMSFields(): FieldList
{
$fields = parent::getCMSFields();
// The scaffolded dropdown isn't useful — replace it with the upload field
$fields->removeByName('HeaderVideoID');
$fields->addFieldToTab('Root.Main',
BunnyUploadField::create('HeaderVideoID', 'Header video')
->setDescription('Upload a new video or pick an existing one.')
);
return $fields;
}
}In the CMS the field lets an editor upload a new video or select an already-uploaded one from a dropdown. Once attached, a preview shows the thumbnail with Bewerken (open the video in the video admin) and Ontkoppelen (detach) actions.
Output the relation directly and it renders the default player — no per-page PHP
needed. For a different style, pass a preset name to .Embed():
<%-- Common in-content player: native controls, no autoplay --%>
$HeaderVideo
<%-- Muted looping background/hero: autoplay, no controls --%>
$HeaderVideo.Embed('autoplay')Both render an empty string when no video is attached, so they're safe to call
unconditionally. $HeaderVideo is shorthand for $HeaderVideo.Embed('default').
See Embed presets below to tweak the presets or add your own.
$Video.Embed('name') renders a named preset (and bare $Video renders the
default one). Presets are plain option sets (autoplay, muted, loop,
controls) defined in YAML, so you can retune them or add your own without touching
PHP. The two built-ins:
| Preset | Behaviour |
|---|---|
default |
Native controls, no autoplay — the normal in-content player. |
autoplay |
Muted, looping, no controls — background/hero video. |
Override a built-in or add your own from your project's YAML config:
# app/_config/bunnystream.yml
Restruct\BunnyStream\Model\BunnyVideo:
embed_presets:
# New preset — use as $Video.Embed('hero')
hero:
autoplay: true
muted: true
loop: true
controls: false
# Retune a built-in — e.g. let the background preset show controls
autoplay:
autoplay: true
muted: true
loop: true
controls: trueAn unknown preset name falls back to default behaviour.
Useful methods on Restruct\BunnyStream\Model\BunnyVideo:
| Method | Returns |
|---|---|
Embed(string $preset = 'default') |
Template-safe responsive embed for a named preset. Empty when no video is attached. Bare $Video calls this via forTemplate(). |
getPlayerIframeHTML(array $options = []) |
Low-level <iframe> builder behind Embed. Options: autoplay, muted, loop, controls. |
getPlayerURL() |
The (optionally signed) embed URL. |
getThumbnailUrl() |
The poster/thumbnail URL. |
isReady() |
true once Bunny has finished transcoding. |
getStatusLabel() |
Human-readable status. |
getDurationFormatted() |
e.g. 1:23. |
refreshFromApi() |
Pull the latest metadata from Bunny. |
getUsages() |
All records that link to this video. |
Per-video player options are editable in the video admin: rememberPosition
(resume at last position), t (start offset, e.g. 90s), and enforceFullWatch
(the viewer can't skip past the furthest point watched).
The module registers a Video's admin menu listing every BunnyVideo with
thumbnail and status. Open a record to edit its title/description, set player
options, upload a custom poster, and see which records use it.
Adding a video — the "Add new" button opens an upload-only screen: pick a file, it uploads straight to Bunny, and you land on the new video's edit form. There's no "save an empty record" step — the record is created by the upload itself (a create submitted without an upload is rejected).
BunnyUploadField::setUploadOnly(true) produces this upload-only variant (no
attach-existing picker, redirects to the created record on success); it's what
the admin's new-record form uses and can be reused wherever a bare "upload a new
video" control is wanted.
- Deletes are fail-closed — deleting a
BunnyVideodeletes the remote video too; if that API call fails the local delete is aborted (an override checkbox is offered on the edit form). - Private-library thumbnails 403 — if the pull zone blocks direct file access,
Bunny thumbnails won't render. Make them public and set
BUNNY_STREAM_CDN_HOSTNAME, or supply your own poster image. - Set
BUNNY_STREAM_TOKEN_AUTH_KEY(and enable Token Authentication on the library) to make embed URLs expire — otherwise anyone with the URL can play the video.
MIT © Restruct · dev@restruct.nl