WP Manifestindependent plugin directory
manifest / media / mux-offload

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.

by Sodigital · github.com/pawankhated/mux-offload

0stars
0forks

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.zip

Readme

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 paste https://stream.mux.com/PLAYBACK_ID/highest.mp4 into their background video field.

Install

  1. Upload the mux-offload folder to /wp-content/plugins/ (or zip it and use Plugins → Add New → Upload).
  2. Activate it.
  3. 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.

  1. Mux dashboard → create an environment per client site
  2. Generate an access token inside that environment
  3. 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

  1. Scan — lists every video attachment with no playback ID, with file sizes and total disk usage.
  2. 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.
  3. Dry run — on by default. Reports what would happen and sends nothing to Mux. Run this first.
  4. Migrate — one video per request with a live progress bar, so a stalled item is visible rather than silently retrying.
  5. Reclaim disk space — a separate, deliberate step. Only touches videos confirmed ready on Mux.

Read the full README on GitHub →