Mux Offload
Stops video files from living on your WordPress server. When you upload a video, it goes to Mux, the local copy is deleted, and the front end renders a Mux Player with adaptive HLS streaming.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/pawankhated/mux-offload/archive/refs/heads/main.zipReadme
Mux Offload
Author: Sodigital Version: 1.0.0 Requires: WordPress 6.0+, PHP 7.4+
Stops video files from living on your WordPress server. When you upload a video, it goes to Mux, the local copy is deleted, and the front end renders a Mux Player with adaptive HLS streaming.
What actually happens on upload
You pick a video in WP
│
▼
WP writes it to /uploads (PHP always does this first)
│
▼ add_attachment hook fires
Plugin creates a Mux "direct upload" slot ──► POST /video/v1/uploads
│
▼ streamed with cURL, never loaded into PHP memory
File is PUT to the signed Mux URL
│
▼ Mux encodes asynchronously
Webhook video.asset.ready (or 1-min cron fallback)
│
▼
Playback ID saved to post meta + local file deleted
│
▼
Front end renders <mux-player playback-id="…">
The attachment post is kept. Only the heavy file is removed, so galleries, links and post content references keep working.
Page builder support
| Builder | Auto-swaps video | How it's handled |
|---|---|---|
| Gutenberg | Yes, out of the box | render_block catches core/video by attachment ID. Also ships a dedicated Mux Video block. |
| Elementor | Yes, out of the box | Dedicated Mux Video widget, plus Elementor's own Video widget is intercepted via elementor/widget/render_content. Background videos rewritten to MP4. |
| Classic editor | Yes, out of the box | the_content filter |
| Divi | Enable Universal mode | Divi renders partly through the_content, but its modules are inconsistent. Universal mode makes it reliable. |
| Avada / Fusion | Enable Universal mode | Same reasoning as Divi. |
| WPBakery | Enable Universal mode | Same. |
| Bricks | Enable Universal mode | Bypasses the_content entirely. |
| Oxygen | Enable Universal mode | Bypasses the_content entirely. |
| Beaver Builder | Enable Universal mode | Bypasses the_content entirely. |
| Anything else | Shortcode always works | [mux id="123"] in any Text / HTML / Shortcode module |
Universal mode (Settings → Behaviour) buffers the whole rendered page and swaps any <video> whose source resolves to an offloaded attachment. It handles src, data-src and nested <source> tags, carries the builder's CSS classes across, and skips builder edit canvases.
Leave it off if Gutenberg and Elementor are your only builders — they're handled natively, and page-level output buffering can conflict with some caching and optimisation plugins.
Known limits
- Builders that inject video via JavaScript after page load (some lightbox and popup modules) aren't caught — the HTML doesn't exist when PHP buffers it. Use the shortcode there.
- Background videos need an MP4 rendition, because builders inject a raw
<video>that can't play HLS. Enable MP4 renditions (Plus tier). Elementor is handled automatically; for Divi/Avada pastehttps://stream.mux.com/PLAYBACK_ID/highest.mp4into their background video field.
Install
- Upload the
mux-offloadfolder to/wp-content/plugins/(or zip it and use Plugins → Add New → Upload). - Activate it.
- Go to Settings → Mux Offload.
Setup
1. API credentials
Mux dashboard → Settings → Access Tokens → Generate new token. Permissions: Mux Video → Read and Write. Paste the Token ID and Secret, save, then hit Test connection.
2. Webhook (recommended)
Mux dashboard → Settings → Webhooks → Create new webhook.
URL: the one shown on the settings page, e.g. https://yoursite.com/wp-json/mux-offload/v1/webhook
Copy the signing secret back into the settings page.
Without this, the plugin polls Mux once a minute instead. That works, but videos take up to 60s longer to flip to "Ready". The webhook needs the site to be publicly reachable — it won't fire on localhost or a password-protected staging site, so those fall back to polling.
3. Signed playback (only if content is gated)
Mux dashboard → Settings → Signing Keys → Generate new key. Set Playback policy to Signed, then paste the Key ID and private key.
Better for security: put the key in wp-config.php instead of the database:
define( 'MUXOFF_TOKEN_ID', 'xxx' );
define( 'MUXOFF_TOKEN_SECRET', 'xxx' );
define( 'MUXOFF_SIGNING_KEY', '-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----' );
These constants override whatever is in the settings screen.
Displaying videos
Block editor
Use the Mux Video block. Or just use the normal Video block — the plugin swaps it for Mux Player at render time.
Elementor
- Mux Video widget — search "Mux" in the widget panel. Controls for autoplay, loop, poster frame, accent colour, max width, border radius, responsive sizing.
- Elementor's own Video widget — set Source to Self Hosted and pick the file. Swapped automatically.
- Shortcode widget —
[mux id="123"]
Shortcode (works in every builder)
[mux id="123"]
[mux id="123" autoplay="true" muted="true" loop="true" controls="false"]
[mux id="123" max_width="900px" poster_time="12" accent_color="#ff0055"]
[mux id="123" aspect_ratio="9/16"]
[mux playback_id="61zK4LlhV9P00tpGpsH7Fc00T58eR7m63b"]
Theme template
echo muxoff_player( $attachment_id );
echo muxoff_player( $attachment_id, array(
'autoplay' => true,
'loop' => true,
'controls' => false,
'aspect_ratio' => '9/16',
) );
How to call things
| You want | You call | Returns |
|---|---|---|
| The Mux stream URL | wp_get_attachment_url( 123 ) |
.m3u8 (or .mp4 if renditions on) |
| The poster image | wp_get_attachment_image_src( 123 ) |
image.mux.com/.../thumbnail.jpg |
| A working player | muxoff_player( 123 ) |
Full <mux-player> HTML |
| Just the playback ID | MuxOff_Uploader::playback_id( 123 ) |
H13iynHi... |
| Is it ready? | get_post_meta( 123, '_mux_status', true ) |
ready / processing / errored |
Important: use wp_get_attachment_url() only when you need a URL (download link, API response, og:video tag). For anything that plays on a page, use muxoff_player() or let the render filters handle it — a bare .m3u8 in a <video> tag only works in Safari.
Settings reference
| Setting | What it does |
|---|---|
| Video quality | basic / plus / premium. Affects encoding quality and cost, not resolution. Basic has no encoding charge and uses a reduced ladder. Plus and Premium use per-title AI encoding. Live streaming requires Plus or higher. |
| Maximum resolution | 1080p (Mux default) / 1440p / 2160p. This is what actually caps resolution. Mux ingests at 1080p by default to avoid surprise 4K bills. Cannot be changed after ingest without re-uploading. |
| Playback policy | Public = anyone with the link. Signed = expiring JWT tokens. |
| Auto captions | Generates subtitles on upload. Small per-minute charge. |
| MP4 renditions | Creates downloadable MP4s. Needed for background videos. |
| Player accent colour | Brand colour for the player controls. |
| Site identifier | Tags assets with their originating site when several sites share one Mux environment. |
| Delete local copy | The whole point of the plugin. Frees your disk. |
| Auto render | Swaps core video blocks and raw <video> tags for Mux Player. |
| Universal builder mode | Page-level swap for builders that bypass the_content. |
| Delete on Mux | Also deletes the Mux asset when you delete the attachment. Off by default — irreversible. |
| Token lifetime | How long a signed playback token stays valid. |
| Debug log | Keeps the last 200 entries, shown at the bottom of the settings page. |
Hooks for developers
// Change what gets sent to Mux on asset creation
add_filter( 'muxoff_asset_settings', function ( $settings ) {
$settings['video_quality'] = 'premium';
$settings['passthrough'] = 'site-' . get_current_blog_id();
return $settings;
} );
// Add or change player attributes
add_filter( 'muxoff_player_attributes', function ( $attrs, $args ) {
$attrs['default-hidden-captions'] = 'true';
$attrs['playback-rates'] = '0.5 1 1.5 2';
return $attrs;
}, 10, 2 );
// Wrap the player markup
add_filter( 'muxoff_player_html', function ( $html, $args ) {
return '<div class="my-video-wrap">' . $html . '</div>';
}, 10, 2 );
// Do something when a video finishes encoding
add_action( 'muxoff_asset_ready', function ( $attachment_id, $asset ) {
// e.g. email the author, clear a cache, publish a course lesson
}, 10, 2 );
Add data-muxoff-skip to a <video> tag to exclude it from Universal mode.
Post meta stored on each attachment
| Key | Value |
|---|---|
_mux_upload_id |
Mux direct upload ID |
_mux_asset_id |
Mux asset ID |
_mux_playback_id |
Playback ID — what the player uses |
_mux_playback_policy |
public or signed |
_mux_status |
queued / uploading / processing / ready / errored |
_mux_duration |
Seconds |
_mux_aspect_ratio |
e.g. 16:9 |
_mux_original_url |
The pre-offload local URL, kept for reverse lookup |
_mux_local_deleted |
1 once the file is gone from your server |
_mux_keep_local |
1 on migrated videos — blocks auto-deletion |
_mux_migrated |
1 if it came from the migration tool rather than a fresh upload |
_mux_error |
Last error message, if any |
File structure
mux-offload/
├── mux-offload.php Bootstrap, constants, cron, activation
├── includes/
│ ├── class-mux-api.php Mux REST wrapper
│ ├── class-mux-jwt.php RS256 signing for signed playback
│ ├── class-mux-settings.php Admin settings screen
│ ├── class-mux-uploader.php Upload interception, status, cleanup
│ ├── class-mux-webhook.php Webhook receiver + signature check
│ ├── class-mux-render.php Player HTML, shortcode, block, filters
│ ├── class-mux-media-library.php Status column, attachment fields
│ ├── class-mux-elementor.php Elementor widget registration
│ ├── class-mux-elementor-widget.php The widget itself
│ ├── class-mux-compat.php Reverse URL lookup, Elementor interception
│ ├── class-mux-universal.php Page-level swap for other builders
│ └── class-mux-migration.php Bulk migration of existing videos
└── assets/
├── admin.js Test connection, retry, status polling
├── admin.css
├── block.js Gutenberg block, no build step
└── migration.js Migration screen UI
Using one Mux account across multiple sites
Mux has no folders. Assets are a flat list. There are two ways to keep client sites apart.
Recommended: separate Environments
An Environment is a container inside your Mux organization with its own isolated assets, access tokens, signing keys and webhooks. A token created in one environment cannot touch assets in another, and webhooks configured for one won't fire for the other. Billing stays at the organization level, so you still get one invoice.
- Mux dashboard → create an environment per client site
- Generate an access token inside that environment
- Paste that token into that site's plugin settings
No plugin configuration needed beyond the token — it already works this way.
Why this beats tagging:
- Real isolation. If a client site is compromised, the attacker cannot list or delete another client's videos.
- Clean analytics. Mux Data is per environment, so each client gets their own numbers.
- No webhook cross-talk. Each site only hears about its own assets.
Alternative: site tagging on a shared token
If you deliberately want one shared token, set a Site identifier in the settings. Every asset created from that site is tagged with:
| Field | Value |
|---|---|
meta.creator_id |
Your site identifier (defaults to the domain) |
meta.title |
The attachment title |
meta.external_id |
site-identifier-123, unique across all sites sharing the environment |
passthrough |
{"site":"...","id":123} for programmatic filtering |
You can then filter by creator ID in the Mux dashboard and in Mux Data.
Caveat: this metadata is readable by the browser through the player. Never put anything sensitive in the site identifier. It also gives you no isolation — every site's token can still see and delete every other site's assets. It is organisation, not security.
// Override the tagging entirely
add_filter( 'muxoff_site_meta', function ( $meta, $attachment_id, $site_id ) {
$meta['creator_id'] = 'client-acme';
return $meta;
}, 10, 3 );
Migrating existing videos
Media → Mux Migration
Sends videos already in your library to Mux. Local files are kept, so migration is reversible.
How it works
- Scan — lists every video attachment with no playback ID, with file sizes and total disk usage.
- Choose a method:
- URL pull (default) — Mux fetches the file from your site's public URL. No outbound bandwidth from your server, no PHP timeout risk. Needs the site publicly reachable.
- Upload from server — streams the file up, same path as a new upload. Use this on staging or password-protected sites.
- Dry run — on by default. Reports what would happen and sends nothing to Mux. Run this first.
- Migrate — one video per request with a live progress bar, so a stalled item is visible rather than silently retrying.
- Reclaim disk space — a separate, deliberate step. Only touches videos confirmed
readyon Mux.