restruct / silverstripe-bunnystream
Bunny Stream video integration for Silverstripe: upload, manage and embed videos
Package info
github.com/restruct/silverstripe-bunnystream
Type:silverstripe-vendormodule
pkg:composer/restruct/silverstripe-bunnystream
Fund package maintenance!
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.3
- silverstripe/admin: ^2 || ^3
- silverstripe/assets: ^2 || ^3
- silverstripe/framework: ^5 || ^6
- silverstripe/vendor-plugin: ^2 || ^3
Requires (Dev)
- silverstripe/recipe-testing: ^3 || ^4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Maintained by Restruct. If this module saves you time, you can support ongoing maintenance.
Upload, manage and embed videos hosted on Bunny Stream from the
Silverstripe CMS. The video file never touches your server: the browser uploads it straight to
Bunny (resumable TUS upload), and Silverstripe keeps a BunnyVideo record with the video's GUID and
the metadata Bunny reports back.
What you get:
BunnyVideo, a DataObject per video: title, description, status, duration, dimensions, storage size, an optional poster image, and per-video player options.BunnyUploadField, a form field for ahas_onetoBunnyVideo: pick a file, it uploads to Bunny with a progress bar, and the relation is set when it finishes.VideoAdmin, a "Video's" section in the CMS listing every video with its thumbnail, status, duration, size and dimensions.BunnyStreamClient, a small client for the Bunny Stream API (create, get, list, delete), TUS upload signing and (signed) embed URLs.
The CMS labels are Dutch and not translatable yet.
Requirements
- Silverstripe 5 or 6 (
silverstripe/framework,silverstripe/admin,silverstripe/assets) - PHP 8.1 or newer
- A Bunny Stream video library
Installation
composer require restruct/silverstripe-bunnystream
Set the environment variables below, then run sake dev/build flush=1 (Silverstripe 5) or
sake db:build --flush (Silverstripe 6).
Version compatibility
| Branch | Module version | Silverstripe | PHP |
|---|---|---|---|
main |
1.x |
^5 || ^6 |
^8.1 |
| (tags only) | 0.1.x |
^5 |
^8.1 |
main is the only maintained line: it supports every Silverstripe version this module targets, so
there is no separate maintenance branch. A version branch will be created only when a change cannot
be made compatible across the supported range. Silverstripe 4 is not supported.
composer.json is the source of truth for exact constraints; this table is a quick reference.
Upgrading from 0.1.x: see UPGRADING.md.
Configuration
All configuration is through environment variables (.env):
| Variable | Required | What it does |
|---|---|---|
BUNNY_STREAM_API_KEY |
yes | The library's API key (Bunny dashboard > Stream > your library > API). |
BUNNY_STREAM_LIBRARY_ID |
yes | The numeric library ID. |
BUNNY_STREAM_CDN_HOSTNAME |
no | Pull-zone hostname for thumbnails. Default: vz-<library ID>.b-cdn.net. Embeds always use iframe.mediadelivery.net. |
BUNNY_STREAM_TOKEN_AUTH_KEY |
no | The library's "Token Authentication Key". When set, and token authentication is enabled on the library, embed URLs are signed and expire (4 hours by default). Without it, anyone with the embed URL can play the video. |
Usage
Attach a video to a record
Add a has_one to BunnyVideo and use BunnyUploadField for it:
use Restruct\BunnyStream\Forms\BunnyUploadField; use Restruct\BunnyStream\Model\BunnyVideo; class Lesson extends DataObject { private static $has_one = [ 'BunnyVideo' => BunnyVideo::class, ]; public function getCMSFields() { $fields = parent::getCMSFields(); $fields->replaceField( 'BunnyVideoID', BunnyUploadField::create('BunnyVideoID', 'Video') ->setDescription('MP4, MOV, ...') ); return $fields; } }
What happens on upload:
- The field calls its
createUploadaction, which creates the video on Bunny, writes aBunnyVideorecord (status "created") and returns short-lived TUS credentials. - The browser uploads the file directly to Bunny with tus-js-client (loaded from jsDelivr), showing progress.
- When it finishes, the new record's ID is put in the field, so saving the form sets the relation.
With a video attached the field shows its thumbnail, title, status and duration, and an "Ontkoppelen" button that clears the relation (the video itself is kept).
createUpload refuses anyone without CMS access (403: ADMIN or any CMS_ACCESS_* code is
needed) and any request without the form's security token (400); the field's JavaScript sends the
token as SecurityID. Do not put the field on a public front-end form: visitors and members
without CMS access are refused, so it cannot work there.
Embed a video
$video->getPlayerIframeHTML(); // responsive 16:9 <iframe> in a .ratio wrapper $video->getPlayerIframeHTML(['autoplay' => true, 'muted' => true, 'loop' => true, 'controls' => false]); $video->getPlayerURL(); // the embed URL alone, signed when configured $video->getThumbnailUrl();
getPlayerIframeHTML() returns '' when the record has no video. autoplay is always sent
(default false) so a library-level autoplay setting does not apply unless you ask for it. In a
template, $BunnyVideo.PlayerIframeHTML outputs the markup (it is cast as HTML).
Per-video player options
Each video carries a small JSON store of player options, editable in the CMS under "Afspeelopties":
| Option | CMS field | Default | Effect |
|---|---|---|---|
rememberPosition |
Positie onthouden | on | Bunny embed parameter: resume where the viewer stopped. Always sent, so a per-video "off" overrides the library setting. |
t |
Starttijd | none | Bunny embed parameter: start offset, eg 90s, 1m30s, hh:mm:ss. |
enforceFullWatch |
Volledig bekijken afdwingen | off | Emitted as a data-enforce-full-watch="1" attribute on the iframe. The module does not enforce it: Bunny has no "disable seeking" parameter, so your front-end JavaScript has to read the attribute and act on it (eg through Bunny's player.js API). |
From PHP: getPlayerOption($key, $default), setPlayerOption($key, $value) (null removes the
key), getPlayerQueryParams() and getPlayerDataAttributes(). Other keys can be stored too; only
the three above are acted on.
Status and metadata
Bunny processes a video asynchronously (created, uploaded, processing, transcoding, finished, or
error / upload failed). Until a video is finished, opening it in the CMS refreshes its metadata from
the API; an unreachable API leaves the stored values in place rather than breaking the form. Call
refreshFromApi() yourself to sync outside the CMS (it writes the record). isReady() is true once
Bunny reports the video finished.
Usages
A video's edit form has a "Gebruikt door" tab listing every record that points at it through a
has_one, found by scanning the data model, so the module needs no configuration for your classes.
The same list is available as getUsages().
Deleting
Deleting a BunnyVideo deletes the video on Bunny first. If that fails, the local delete is
aborted (a ValidationException is thrown) and the error is remembered in the session: the next
time the video is opened, the form shows the error and a "Forceer lokale verwijdering" checkbox.
Ticking it and saving makes the next delete skip the API (a warning is logged, and the remote video
is left behind for manual cleanup). A record without a GUID is simply deleted locally.
The API client
use Restruct\BunnyStream\Api\BunnyStreamClient; $client = BunnyStreamClient::create(); // credentials from the environment $client->createVideo('Title', $collectionId = null); $client->getVideo($guid); $client->listVideos($page = 1, $perPage = 100, $search = null); $client->deleteVideo($guid); $client->isReady($guid); $client->getTusUploadCredentials($guid, $expiresInSeconds = 3600); $client->getEmbedUrl($guid, $expiresInSeconds = 14400);
The module always instantiates the client through the Injector, so a project can replace it:
SilverStripe\Core\Injector\Injector: Restruct\BunnyStream\Api\BunnyStreamClient: class: App\Video\LoggingBunnyClient
API errors surface as Guzzle exceptions.
Running the tests
The suite needs a booted Silverstripe project. Install the module into a host project as a
symlinked path repository (/tests is export-ignore, so a dist install has no tests), then from
the host root:
# Silverstripe 5 (PHPUnit 9): flush with a trailing flush=1 AFTER the test path
vendor/bin/phpunit vendor/restruct/silverstripe-bunnystream/tests flush=1
# Silverstripe 6 (PHPUnit 11): flush through the environment
SS_PHPUNIT_FLUSH=1 vendor/bin/phpunit vendor/restruct/silverstripe-bunnystream/tests
No request reaches Bunny: the tests swap in a mock HTTP transport. .github/workflows/ci.yml builds
such a host project per Silverstripe major.
License
MIT, see LICENSE.