Mux ado about streaming, mate!
MuxMate integrates Mux with Craft CMS.
This plugin requires Craft CMS 5.2.0 or later, and PHP 8.2 or later.
This is a private plugin, made for Værsågod and friends.
Register a Mux account on https://www.mux.com, and add an environment. It's a good idea to have separate Mux environments for each Craft environment.
Create a Mux access token for the appropriate Mux environment. The token only needs the "Mux Video" permission.
Copy the access token ID and secret key.
Create a file config/_muxmate.php
in your repository, and add the Mux access token ID and secret key:
<?php
use craft\helpers\App;
return [
'muxAccessTokenId' => App::env('MUX_ACCESS_TOKEN_ID'),
'muxSecretKey' => App::env('MUX_SECRET_KEY'),
];
...there are heaps more config settings (see below for all of them), but those two are the only ones you'll definitely need.
You're going to have a hard time without a working Mux webhook, so don't skip this step!
In your Mux account, visit Settings -> Webhooks, select the appropriate environment, and create a new webhook with this URL:
https://your-site.com/muxmate/webhook
If this is a local environment (or otherwise not accessible from outside), you can use Ngrok or similar to create a publicly available Webhook endpoint URL.
MuxMate ships with a custom "MuxMate" field type. Create a field with this field type (name or handle doesn't matter), and add it to the field layout for any volume(s) where you plan on uploading your videos.
When a video (with a MuxMate field in its field layout) is saved in Craft, it's URL is automatically POSTed to Mux's assets/create
endpoint. Assuming the video's URL is publicly available, Mux then downloads the video and creates a Mux asset for it.
The process on Mux' end is completely asynchronous, but follows a pre-determined order:
- Mux asset is created, with basic metadata
- HLS stream is created; Mux asset status changes to "ready" when this is ready
- Static renditions (i.e. transpiled MP4 assets) are created; the Mux asset metadata will contain a key
static_renditions
when this is done.
Assuming the MuxMate webhook URL has been added to Mux (and is publicly available), the asset's Mux meta data in Craft will be automatically updated for each of the above steps.
To render a video that has been created in Mux, simply call the getMuxVideo()
method on the asset to render a custom <mux-video>
web component instance:
{{ video.getMuxVideo()|attr({ controls: true }) }}
In local environments, Mux typically won't be able to access your videos. To make sure it can, either
A simple solution is to use some sort of cloud storage file system for your videos' volume. You can even set up a cloud storage file system that is only used locally, by using an environment variable for your volume's "Asset Filesystem" setting.
If you've already set up something like an Ngrok tunnel for your webhook, you can also use that same tunnel to ensure Mux is provided access to your video assets. Here's a couple of ways to do that:
You can configure MuxMate to override the base URL for assets in a particular volume, by using the MuxMate volumes
config setting.
Assuming your videos live in a volume with the handle videos
(which is using a file system with the Base URL set to @web/uploads/videos
), here's how you could override that Base URL with your Ngrok URL:
In your .env
file:
VIDEO_VOLUME_BASE_URL=https://2d13-51-175-241-107.ngrok-free.app/uploads/videos
In your config/_muxmate.php
file:
<?php
use craft\helpers\App;
return [
'muxAccessTokenId' => App::env('MUX_ACCESS_TOKEN_ID'),
'muxSecretKey' => App::env('MUX_SECRET_KEY'),
'volumes' => [
'videos' => [
'baseUrl' => App::env('VIDEO_VOLUME_BASE_URL'),
],
],
];
Another option is to add a custom Yii alias, for example something like @videosBaseUrl
, and set your volume's file system's "Base URL" setting to that alias (for example, @videosBaseUrl/uploads/videos
). Then, define the custom @videosBaseUrl
alias using the aliases
Craft config setting:
In your .env
file:
VIDEOS_BASE_URL=https://2d13-51-175-241-107.ngrok-free.app
In your config/general.php
file:
'aliases' => [
'@videosBaseUrl' => App::env('VIDEOS_ALIAS') ?? App::env('PRIMARY_SITE_URL')
],
MuxMate assets are created when videos are uploaded to Craft, and simply adding a MuxMate field to a volume won't do anything, in terms of any existing video assets in that volume. If you want MuxMate to create (or update) Mux assets for existing Craft video assets, there are a few options:
MuxMate does not create or update Mux assets when Craft assets are re-saved programmatically (this is on purpose). But, resaving an asset manually via the control panel will do it.
Unlike resaving the asset manually, this will not update the asset's dateUpdated
attribute, to the extent that matters.
The _muxmate/create
CLI command can be used to create Mux assets, both in bulk (i.e. for an entire volume) or for a single asset:
_muxmate/create
- Create Mux assets for all video assets. Pass --update=1
to also re-create any existing Mux assets.
_muxmate/create --volume=videos
- Create Mux assets for all video assets in a volume videos
. Pass --update=1
Pass --update=1
to also re-create any existing Mux assets.
_muxmate/create --assetId=1234
- Create a Mux asset for a video asset with the ID 1234
. Pass --update=1
Pass --update=1
to also re-create an existing Mux asset.
The easiest way to render Mux videos is by using the getMuxVideo()
method, which is available to all assets:
{{ video.getMuxVideo() }}
The above will render a <mux-video>
web component, which is a near-drop in replacement for the native <video>
element.
The good thing about <mux-video>
is that it automatically polyfills HLS if the user's browser doesn't support it, and that it automatically integrates with Mux Data.
The |attr()
Twig filter can be used to set attributes on the <mux-video>
component:
{{ video.getMuxVideo()|attr({
id: 'my-video',
class: 'border-2 border-black',
controls: true
}) }}
To render an inline video (i.e. video loop), you can pass a parameter inline: true
to the getMuxVideo()
method:
{{ video.getMuxVideo({ inline: true }) }}
The above automatically sets the following attributes on the <mux-video>
component:
autoplay: 'muted',
loop: true,
muted: true,
playsinline: true,
disablePictureInPicture: true
Alternatively, you can of course set these attributes yourself using the |attr()
Twig filter:
{{ video.getMuxVideo()|attr({
autoplay: 'muted',
loop: true,
muted: true,
playsinline: true,
disablePictureInPicture: true
}) }}
Styling <mux-video>
is generally straight-forward. One exception is object-fitting, since object-fit
needs to be applied to the actual <video>
element – which can't be accessed as it's nested in the web component's shadow DOM.
By default, Mux videos are object-fitted using object-fit: contain
. To change the object-fit value, the following CSS vars can be used:
--media-object-fit
--media-object-position
Example using object-fit: cover;
:
{{ video.getMuxVideo({ inline: true })|attr({
style: {
'--media-object-fit': 'cover',
'--media-object-position': '25% 10%'
}
}) }}
Generally, <mux-video>
works the same as native <video>
elements – i.e. it has same methods (.play()
, .pause()
, etc), and emits the same events (timeupdate
etc).
However, before attempting to interact with a Mux video programmatically, you should make sure that the custom <mux-video>
web component has loaded, using the customElements
API:
window.customElements.whenDefined('mux-video')
.then(() => {
const video = document.getElementById('video');
video.addEventListener('canplay', () => {
video.play();
console.log('ready!');
});
})
.catch(error => {
console.error(error);
});
The <mux-video>
web component is a little bit beefy (~150K gzipped), so it can make sense to lazy-load it – i.e. not load it before a <mux-video>
element actually enters the viewport.
To automatically lazy-load the <mux-video>
web component everywhere, simply set the lazyloadMuxVideo
config setting to true
:
<?php
return [
'lazyloadMuxVideo' => true,
];
For more advanced use cases, you can prevent MuxMate from automatically loading the <mux-video>
web component at all, by setting the muxVideoUrl
config setting to false
:
<?php
return [
'muxVideoUrl' => false,
];
At that point, you're free to load the web component yourself in whatever lazy fashion you fancy. For example:
const video = document.getElementById('video');
let hasLoadedMuxVideoJs = false;
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting && !hasLoadedMuxVideoJs) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = 'https://cdn.jsdelivr.net/npm/@mux/mux-video@0';
document.body.appendChild(script);
}
});
If you're implementing a CSP (good idea!), you might need to set a nonce to the script tags created by MuxMate. To do that, use the scriptSrcNonce
config setting. Here's an example using the ToolMate plugin to create the nonce:
<?php
use \vaersaagod\toolmate\ToolMate;
return [
'scriptSrcNonce' => ToolMate::getInstance()->csp->createNonce('script-src'),
];
This feature works even with template caching enabled 🔥
The getMuxImageUrl()
asset method supports all the same parameters as described in the Mux documentation: https://docs.mux.com/guides/video/get-images-from-a-video.
Example:
{% set imageUrl = video.getMuxImageUrl({ width: 1080, height: 720 }) %}
<img src="{{ imageUrl }}" />
The getMuxGifUrl()
asset method supports all the same parameters as described in the Mux documentation: https://docs.mux.com/guides/video/get-images-from-a-video#get-an-animated-gif-from-a-video.
Example:
{% set gifUrl = video.getMuxGifUrl({ start: 10, end: 20, width: 300, height: 150 }) %}
<img src="{{ gifUrl }}" />
In order to protect your Mux assets, signing the Mux URLs can be a good idea.
The first thing you'll need to do in order to start working with Mux signed URLs, is creating a Mux Signing Key:
- In your Mux account, navigate to Settings -> Signing Keys
- Select the proper environment, then click "Generate new key"
- Copy the Signing Key ID and Base64-encoded Private Key (do not download as a .pem file)
- Configure MuxMate to use the signing key:
<?php
return [
...
'muxSigningKey' => [
'id' => App::env('MUX_SIGNING_KEY_ID'),
'privateKey' => App::env('MUX_SIGNING_PRIVATE_KEY'),
],
],
MuxMate creates public and signed playback IDs for all your assets, so using the signed ID is simply a matter of selecting the proper policy.
By default, MuxMate uses the public playback IDs for all assets. To use the signed playback IDs by default, configure MuxMate's defaultPolicy
config setting:
<?php
return [
...
'defaultPolicy' => 'signed',
];
It's also possible to select a specific policy whenever you create a video stream, a URL to a static rendition or an image/GIF, by passing a parameter signed
.
Some examples:
{% set imageUrl = video.getMuxImageUrl({ width: 1080, height: 720 }, 'signed') %}
<img src="{{ imageUrl }}" />
{% set gifUrl = video.getMuxGifUrl({ start: 10, end: 20, width: 300, height: 150 }, 'signed') %}
<img src="{{ gifUrl }}" />
{{ video.getMuxVideo({ inline: true }, null, 'signed') }}
See Mux asset methods and attributes for a complete overview of the methods that support the signed
parameter.
It's possible to execute Asset sub queries based on Mux metadata by using the MuxMate field's handle, for example:
{% set videos = entry.videos.muxMateFieldHandle({ status: 'ready' }).all() %}
Additionally, the :empty:
and :notempty:
directives are supported and can be ensured that assets returned has Mux data in the MuxMate field:
{% set videos = entry.videos.muxMateFieldHandle(':notempty:').all() %}
MuxMate is configured by adding a file config/_muxmate.php
to your repository.
Default: null
The Mux access token ID
Default: null
The Mux access token secret key
Default null
The signing key ID and private key to use for signing URLs.
'muxSigningKey' => [
'id' => App::env('MUX_SIGNING_KEY_ID'),
'privateKey' => App::env('MUX_SIGNING_PRIVATE_KEY'),
'minExpirationTime' => 'PT5M',
],
Default null
(defaults to 'public'
)
The default playback policy to use when generating Mux URLs. Should be set to 'public'
(default) or 'signed'
.
Default null
(defaults to 'high'
)
The default quality to use for static renditions. Needs to be one of 'high'
(default), 'medium'
or 'low'
.
Default null
The default max_resolution
param to use for HLS streams. Needs to be one of '720p'
, '1080p'
, '1440p'
or '2160p'
.
Default null
(defaults to '1080p'
)
The default max_resolution_tier
param to use when creating new Mux assets. Needs to be one of '1080p'
, '1440p'
or '2160p'
.
Default: 'https://cdn.jsdelivr.net/npm/@mux/mux-video@0'
The URL to the <mux-video>
JS library. Set it to a different URL (aliases and environment variables are supported) if you want to use a different distribution, or set it to false
to handle loading the library completely yourself (i.e. for custom lazy loading purposes or the like).
Default: false
Set to true
to make MuxMate lazy load the <mux-video>
library. I.e. instead of automatically being loaded at pageload (albeit async), MuxMate will create an IntersectionObserver and load the script as soon as a <mux-video>
component enters the viewport.
Default null
Default: null
Per-volume config settings. Currently only supports a baseUrl
setting, see "Override your volume's file system Base URL in local environments" above.
MuxMate adds some methods and attributes to asset models:
Returns true
if the asset has a Mux asset ID.
Returns true
if the asset has a Mux asset ID and a "ready" status, i.e. is ready to play.
getMuxVideo(array|null options = null, array|null params = null, string|null policy = null)
[Markup|string]
@options
Array of options; inline
, lazyload
@params
Array of Mux parameters
@policy
One of 'public'
, 'signed'
If the asset has a Mux playback ID, returns a <mux-video>
web component.
The options
array can contain the following settings:
inline: true # Automatically sets all the required attributes for videos that should play inline.
lazyload: true # Lazyloads the `<mux-video>` web component. Defaults to the `lazyloadMuxVideo` config setting
The params
array can contain Mux video params (i.e. "playback modifier"), such as the max_resolution
param:
{{ asset.getMuxVideo(null, { max_resolution: '720p' }) }}
@params
Array of Mux parameters
@policy
One of 'public'
, 'signed'
If the asset has a Mux playback ID, returns a HLS stream URL. The @params
array is the same as for the getMuxVideo()
method.
@quality
Oone of 'high'
, 'medium'
, 'low'
@policy
One of 'public'
, 'signed'
If the asset has a Mux playback ID and static renditions, returns the URL to an MP4 file.
If the quality
param is null or set to a quality that isn't available, the highest available quality is returned.
@params
Array of Mux parameters, see https://docs.mux.com/guides/video/get-images-from-a-video#thumbnail-query-string-parameters
@policy
One of 'public'
, 'signed'
If the asset has a Mux playback ID, returns the URL to a still frame from the video.
@params array
Array of parameters, see https://docs.mux.com/guides/video/get-images-from-a-video#animated-gif-query-string-parameters
@policy
One of 'public'
, 'signed'
If the asset has a Mux playback ID, returns an animated GIF from the video.
Returns the video duration, in seconds
Returns an array of the available static renditions for the video. The array is indexed by quality ('high'
, 'medium'
and 'low'
).
Returns the Mux asset ID.
@policy
One of 'public'
, 'signed'
Returns a Mux playback ID for the given policy.
Returns the entire Mux asset metadata payload. See https://docs.mux.com/api-reference#video/operation/get-asset
Returns the Mux asset status. If it says ready
, you're good to go.
Returns the aspect ratio for the video