Version 2.6.0

October 21, 2026

Added

  • polymedia/migrate/prune-folders lists the empty legacy folders that polymedia/migrate/sidecars leaves behind, and deletes them with --apply. A legacy folder is named after a .pmedia beside it plus 8 letters or digits, or sits inside one with a name that ends the same way. It's deleted only when it holds no assets, trashed or not, no other folders and no files on disk that Craft never indexed. Nested folders go deepest first, and every folder that stays is listed with the reason. (#23)

Changed

  • Replacing or removing a poster on the asset edit screen now deletes the old one when it's a file in the item's sidecar folder that nothing else uses, such as a fetched poster or an inline upload. Library images stay where they are. (#27)

Version 2.5.1

October 21, 2026

Fixed

  • Bunny posters now follow thumbnail changes made in Bunny. Imports, webhooks and polymedia/bunny/sync notice a new thumbnail file, and polymedia/bunny/sync also checks the thumbnail's ETag or Last-Modified, so it notices a thumbnail replaced at the same URL too. Polymedia then replaces the poster it fetched, and deletes the old file when nothing else uses it. A poster an editor chose is never replaced. New posters record where they came from in the item's posterSource metadata; posters fetched before 2.5.1 aren't tracked, so remove one from its asset and sync to fetch it again. (#25)
  • Control panel thumbnails of .pmedia assets without a poster now use the thumbnail URL stored on the item, and derive one from the provider only when none is stored. Mux items imported before 2.5.0, which stored the first frame, still get Mux's own thumbnail. (#26)

Version 2.5.0

October 21, 2026

Added

  • Bunny Stream hosting for Pro: library browsing and search, import, signed resumable direct uploads (tus), status, duration, dimension, MP4 and poster syncing, and polymedia/bunny/sync. Imported videos keep the library ID, video ID and native *.b-cdn.net host they were imported with. (#10, #12)
  • A Video Hosting Provider setting (Mux or Bunny Stream) for new imports and uploads. Authors get provider-neutral Browse video library and Upload video actions. Existing assets keep their own provider. (#8, #11)
  • A per-provider Default Playback Format (HLS or MP4 when available) and a playbackFormat option on player() and element(), rendered by both player engines. MP4 picks the highest ready rendition and falls back to HLS. New Mux uploads request a static rendition only when MP4 is preferred, and Mux rendition webhooks refresh renditions. (#9)
  • Optional signed Bunny Stream webhooks at actions/polymedia/webhooks/bunny, enabled by a Bunny Webhook Verification Key environment reference. (#13)
  • Optional deletion of the Bunny video when its .pmedia is permanently deleted, off by default. Trashing never deletes the video. (#14)
  • Add video or audio → From existing asset (Lite) references a public MP4, WebM or MOV asset by UID without copying or owning it. Twig and GraphQL resolve the source's current URL. (#15)
  • Pro, opt-in Automatically host uploaded MP4 videos for selected volumes: new MP4s from Craft's standard uploader go to the hosting provider and land as .pmedia assets. Mixed batches keep Craft's queue behavior; Craft Cloud volumes can't be routed because Cloud has its own uploader, and if another routed volume's filesystem type has a custom uploader, native uploads are disabled for every volume of that type rather than storing raw MP4s. (#16)
  • scripts() accepts a plain provider list, such as scripts(['mux', 'bunny']).
  • An Add video or audio menu on Polymedia fields and on Assets fields that explicitly allow the Polymedia file kind, between Add an asset and Upload a file. It offers the items the field's allowed providers accept, puts the new or reused .pmedia in the field's upload location and selects it without opening the asset picker, in Matrix, nested entries and slideouts too. On a new entry whose upload location can't be resolved yet, the .pmedia goes to your temporary uploads and moves into place on save, like Craft's own uploads. It hides while the field is full, on Assets fields with Allow uploads off, and when you can't save assets to the field's upload location. (#22)

Changed

  • Add media is now Add video or audio on the Assets index and in asset pickers. A field's picker offers only the items its field allows, and From URL for a field only offers and saves the field's allowed providers.
  • Asset selector modals with no field to select into, such as the one From existing asset opens, no longer show the menu. Uploading a video from there failed with "The field cannot accept another media item".
  • New .pmedia assets record the user who created them as their uploader, as uploaded files do.
  • A field restricted to one location with subfolders allowed now accepts media anywhere under that location, not only in its default upload subfolder.
  • Mux posters and library thumbnails use Mux's own thumbnail, the asset's chosen thumbnail time or else the middle of the video, so they match the Mux dashboard instead of showing the first frame, which is often blank. Posters already fetched don't change. Mux::firstFrameThumbnailUrl() is deprecated in favour of Mux::thumbnailUrl().
  • Control panel thumbnails of .pmedia assets keep their poster's aspect ratio instead of a square crop.
  • Imported and uploaded videos show their media URL and type as read-only technical details on the asset edit screen.
  • Poster fetches no longer follow HTTP redirects.
  • An explicit manifest destination that's missing or not writable is refused instead of being redirected to another volume.
  • Mux upload status and completion now require the signed upload context from preflight, and the .pmedia lands in the folder authorized at the start of the upload.
  • For Mux, Bunny and MP4 items, mediaAttrs can no longer override src, playback-id or type.

Upgrade notes

  • Nothing changes until you opt in: Mux stays the provider, HLS stays the playback format, and automatic routing stays off.
  • No database migration, MP4 backfill, re-encode or remote deletion runs on upgrade.
  • Bunny support covers anonymous playback from native *.b-cdn.net hosts only. Bunny's hosted player, token authentication, referrer restrictions, DRM and custom CDN domains aren't supported.

Version 2.4.0

October 21, 2026

Added

  • Optional Video.js v10 player engine. A new Player Engine setting (env-var capable, so one environment can try it first) switches player(), element() and scripts() site-wide from Media Chrome to Video.js. Players use a packaged skin (Video.js Skin setting or a skin option), reserve their box with the stored aspect ratio (aspectRatio option to override), translate to the site language, and accept your own controls through children. craft.polymedia.playerEngine reports the active engine. Examples in examples/videojs/ show a brand theme and a Media Chrome look-alike that keeps a site's Media Chrome styling. Scripts load from @videojs/cdn pinned to the Video.js Version setting. Media Chrome stays the default, and its markup is unchanged. (#3)

Version 2.3.0

October 17, 2026

Added

  • Mux items now store the aspect ratio Mux reports (metadata.aspectRatio, e.g. 16:9) and the video track's dimensions from import, upload, sync-status, and webhooks. New craft.polymedia.aspectRatio(asset) / asset.getAspectRatio() return it as a CSS aspect-ratio value (16 / 9) so templates can reserve the player's box before the video loads. Run craft polymedia/mux/sync-status to fill it in for existing items. (#5)

Fixed

  • scripts() loaded PeerTube from peertube-video-element@0, which was never published. It now loads @1.
  • The GraphQL width and height fields always returned null because nothing stored them. Mux items now fill them.
  • A forced poster re-fetch (PosterFetcher::fetchForItem(..., force: true)) no longer fails with “A file with the name … already exists”. Poster filenames now include a hash of the image, so the replacement saves beside the old poster and gets a new URL that CDNs haven't cached. The replaced poster is deleted once the new one is attached, but only when it is a Polymedia-managed file in the item's sidecar folder that nothing else uses. (#4)

Version 2.2.0

September 24, 2026

Existing installations need a dedicated sidecar volume before Polymedia can create new managed posters or accept inline poster and WebVTT uploads. Run php craft polymedia/setup/sidecar-volume, or select a dedicated volume in Polymedia settings. Its storage path or bucket prefix must not overlap another volume. Existing media and attachments continue to work.

See the sidecar setup and migration guide.

Mux token settings now require environment-variable references. If your settings contain literal credentials, move those values into environment variables and use references such as $MUX_TOKEN_ID and $MUX_TOKEN_SECRET. Literal credentials will be rejected when saving plugin settings.

Added

  • A dedicated sidecar volume for posters and WebVTT files created by Polymedia. New installations create a public local volume automatically. S3, Google Cloud Storage and other Craft filesystems are also supported.
  • polymedia/setup/sidecar-volume creates the default local sidecar volume on existing installations.
  • polymedia/migrate/sidecars flattens legacy manifest folders and can relocate unshared managed files between sidecar volumes. It supports --dry-run, leaves uncertain or shared attachments in place, and reports failed moves.
  • Mux library search by title, asset ID, playback ID and passthrough text. Search covers the 1,000 most recent videos and shows a notice when the library exceeds that limit.
  • Optional signed Mux webhooks for Pro. Ready, errored, updated and deleted events update existing media items. Invalid or stale signatures are rejected; unknown assets are ignored.
  • polymedia/mux/sync-status refreshes imported Mux items without a webhook endpoint. Use --mux-asset-id to target one video.
  • Local webhook setup instructions for the Mux CLI and public tunnels.

Changed

  • New .pmedia manifests stay directly in the editor's chosen folder. Managed posters and tracks use stable asset-UID folders in the sidecar volume, which is hidden from normal asset browsing.
  • Mux imports, uploads, webhooks and console sync share the same locked state-update path. Re-importing an item also refreshes its status and duration.
  • Mux token and webhook settings require environment-variable references rather than literal credentials.
  • Retired polymedia/migrate/folders, which created the previous storage layout.

Fixed

  • Hard-deleting a moved manifest no longer deletes its containing library folder. Cleanup targets only its owned sidecar folder.
  • Adding media from a field picker now selects it into the field, including eligible assets outside the current page or source.
  • Picker selection respects disabled assets, field limits and selection criteria. Failed replacement rendering leaves the old value intact.
  • Background poster fetching preserves intervening editor choices and avoids duplicate generated posters.
  • Webhooks return HTTP 503 when a lock or state write fails, allowing Mux to retry. Console sync reports those failures instead of counting them as successful.
  • Manifest creation excludes the sidecar volume and handles duplicate titles without filename collisions.
  • Migration preserves earlier successful results when a later operation fails. Retrying skips completed moves.
  • Settings saves and automatic sidecar setup preserve environment-variable references.

Upgrade notes

  • Preview legacy migration with php craft polymedia/migrate/sidecars --dry-run. Legacy posters and tracks remain linked in their original locations because older versions did not record ownership. Do not remove folders that still contain retained files.
  • Webhooks remain optional. Upload polling and console sync still work without them.

Version 2.1.3

July 17, 2026

Fixed

  • Browse Mux library pagination. The Next/Previous controls passed page/limit as data on a GET request, which becomes an ignored request body — so every page returned the first assets over again. They are now sent as query params.

Version 2.1.2

July 17, 2026

Fixed

  • The Browse Mux library modal now scrolls when the grid is taller than the viewport. Garnish fades the modal in with an inline display: block that overrode the shell's display: flex, so the body never filled the shell and its overflow was clipped by the fixed-height container. The body height is now set explicitly between the header and footer, so it scrolls and the footer stays reachable.

Version 2.1.1

July 17, 2026

Fixed

  • Browse Mux library and Upload to Mux modals now scroll their content reliably. Craft's .modal .body { overflow: hidden } could win an equal-specificity cascade tie depending on CSS load order and clip the grid; the plugin rules now outrank it.

Version 2.1.0

July 13, 2026

Added

  • GraphQL support. Assets expose a polymedia field on the Asset GraphQL interface (null for non-.pmedia assets) with type, providerId, element, url, title, duration, width, height, poster, tracks(role, siteId), transcriptUrl, and metadata.
  • “View polymedia data” schema component gates the field per schema (off by default, including the public schema).
  • MediaItems::getByAssetIds() and RelatedAssets::getForItemIds() batch lookups; GraphQL resolution costs a fixed number of queries regardless of item count.

Notes

  • The polymedia handle is now reserved on asset field layouts (a custom field with that handle would collide with the GraphQL interface field).
  • GraphQL itself requires Craft Pro (a Craft constraint, not a plugin one).

Version 2.0.0

July 11, 2026

Added

  • Lite / Pro editions. Lite remains free URL media (including Mux paste). Pro gates Mux credentials UI, library browse, and direct upload.
  • Mux settings: Token ID, Token Secret (env-overridable), and optional “delete from Mux on Craft hard-delete” (default off).
  • Mux service wrapping muxinc/mux-php (isConfigured, list/get assets, direct upload, delete, first-frame thumbnail URL).
  • Browse Mux library CP modal (Pro + credentials): live Mux Assets API grid, “In Craft” badge, import/reuse by playback ID.
  • PosterFetcher downloads provider stills into the item folder and attaches as poster; user-selected posters win.
  • FetchMuxPoster queue job retries Mux first-frame downloads while assets are still processing.
  • CP asset thumbs for .pmedia files use the related poster (or remote thumbnail fallback).
  • MediaItems::getByTypeAndProviderId() / getByTypeAndProviderIds() for import reuse.
  • CP config flag Craft.Polymedia.muxEnabled (Pro + credentials).
  • Upload to Mux CP modal: direct upload via UpChunk, progress bar, poll until playback ID, create/reuse .pmedia, first-frame poster job.
  • Vendored @mux/upchunk in the CP asset bundle.
  • Optional delete Mux asset on Craft hard-delete (default off); soft-delete never calls Mux. Remote delete is synchronous (v1); Lite/unconfigured skips log a warning rather than failing silently.
  • Light Mux status badge + asset id on the asset editor for imported/uploaded items.
  • README: Lite/Pro editions, Mux credentials, browse/upload, poster priority, folder-image limits, public playback.

Changed

  • CP: the separate Add media URL, Browse Mux library, and Upload to Mux buttons are now a single Add media disclosure menu (From URL / Browse Mux library / Upload to Mux) beside Upload files, on both the Assets index and field selection modals.
  • Mux upload modal: Craft-style file picker (hidden input + upload button + filename), a cancel notice, and an auto-sizing shell so progress/status rows never clip.
  • Mux thumbnail URLs use first frame (?time=0) instead of Mux’s mid-video default.
  • autoFetchPoster is implemented and re-enabled in settings (URL create path downloads a poster when none is supplied).
  • Mux library imports always attempt a first-frame poster when the item has no poster.
  • License changed from MIT to the Craft License (proprietary) with the introduction of the commercial Pro edition. Lite remains free to install from the Plugin Store.

Version 1.3.0

July 11, 2026

Added

  • metadata column on polymedia_items so manifest extras (thumbnail URL, provider hints) live in the DB; enables DB-first data() / ManifestWriter::read() without a filesystem round-trip per render.
  • Lazy metadata backfill when reading older rows that only have the .pmedia file populated.
  • options.mediaAttrs on player() / element() to pass attributes through to the inner media element (title, referrerpolicy, etc.). attrs continues to target <media-controller> only.
  • EditorContent service for CP editor fields and element-select configs (extracted from Plugin).
  • Composer scripts: check-cs, fix-cs, phpstan, test.
  • Easy Coding Standard (ecs.php) and PHPStan (phpstan.neon, level 5) config.
  • GitHub Actions CI on PHP 8.2 and 8.3 (phpunit + ECS + PHPStan).

Changed

  • MediaItems::getByAssetId() is memoized per request; cache invalidated on save() / deleteByAssetId().
  • ManifestWriter::read() prefers the DB record; filesystem is fallback + backfill path.
  • savePoster() / saveTracks() logic lives on RelatedAssets; Plugin keeps thin public proxies for BC.
  • Plugin.php slimmed (~1,370 → ~1,070 lines) via EditorContent extraction.
  • Schema version 1.1.0 → 1.2.0 (run php craft up / plugin migrate).

Version 1.2.2

July 11, 2026

Fixed

  • Soft-deleting a polymedia asset no longer drops its MediaItemRecord or related poster/track attachments. Restore from trash keeps metadata intact; cleanup still runs on hard delete (including the dedicated item folder).
  • craft.polymedia.element() now emits the native controls attribute when enabled (default). player() still omits it so Media Chrome controls are not doubled.
  • CDN host and self-host base URL settings with env vars ($VAR / aliases) are resolved via App::parseEnv() in scripts().
  • VTT validation (validateVttOnUpload) runs when attaching tracks from the asset editor; invalid files are skipped.
  • Double-encoded alt on audio cover <img> posters.
  • CP JS strings (Add media URL, Media item created.) are registered for translation.

Changed

  • Settings UI no longer shows unimplemented options: Attachments Volume, Auto-Fetch Poster, Require Captions for Video, Caption Language from Site, Restrict Asset Kinds. Model properties remain for project config BC (removal planned for 2.0).
  • README clarifies that controls applies to element() / getElement(), not player().

Version 1.2.1

May 29, 2026

Fixed

  • craft up could fail when a core or third-party migration re-saved sections before this plugin's m260507_000000_field_settings migration had run. The element-validation hook queried the not-yet-created polymedia_field_settings table and threw The table does not exist. AssetFieldSettings::getAllowedProviders() now returns no restrictions when the table is absent, so migrations complete cleanly.

Version 1.2.0

May 29, 2026

Added

  • Track pickers on the asset edit screen for video items — Captions, Subtitles, and Descriptions. Each is a multi-select .vtt picker that can also upload into the item's folder. Tracks are site-scoped, with srclang and label derived from the current site on save. Previously tracks could only be attached programmatically via RelatedAssets::attach().
  • Poster image picker on the Add media URL create screen, so a poster can be set when first creating an item — previously only available on the asset edit screen. The picker is image-only and supports inline upload, with uploads landing in the item's own folder.
  • Add media URL button on the standalone Assets index toolbar, beside Upload files. Previously only available in field selection modals.
  • Each new .pmedia is now created inside its own dedicated folder (named after the title slug plus a short uid), so its poster and track files sit alongside it instead of cluttering the parent folder. Posters and tracks uploaded inline are co-located into that folder automatically.
  • polymedia/migrate/folders console command to move existing .pmedia items (and their co-located poster/track files) into per-item folders. Supports --dry-run; safe to run repeatedly — items already in their own folder are skipped.

Changed

  • Removed the target-volume dropdown from the Add media URL screen. New .pmedia manifests now land in the volume/folder the user is currently browsing (or the field's upload location), matching how Craft handles normal asset uploads. Falls back to the configured default volume, then the first writable volume.
  • Hard-deleting a .pmedia now also removes its dedicated folder and the poster/track files inside it. A .pmedia that shares a folder with other items leaves the folder untouched.

Version 1.1.0

May 7, 2026

Added

  • "Allowed Providers" setting on plain craft\fields\Assets fields when the polymedia kind is enabled. Mirrors the same filter on PolymediaField, so any Assets field can restrict media URL selections to specific providers. Toggles live as the kind is checked/unchecked.
  • polymedia_field_settings table for per-field plugin settings keyed by field UID.
  • AssetFieldSettings, ProviderFilter services. PolymediaAssetFieldBehavior attached to native Assets fields.
  • Plugin icon (src/icon.svg).

Changed

  • Provider filter validation extracted to a shared service. PolymediaField and plain Assets fields now run identical validation logic.

Removed

  • Unused per-placement poster override code (FieldOverrides service, FieldRelationRecord, polymedia_field_relations table). The schema, service, and allowPosterOverride config key were never reachable from Twig or the CP UI.

Version 1.0.2

May 6, 2026

Added

  • PolymediaAssetBehavior attached to all Asset elements, exposing getPlayer(), getElement(), getData(), getPoster(), getTracks(), getTranscript(), and getIsPolymedia() directly on assets in Twig.

Version 1.0.1

May 6, 2026

Fixed

  • Restored "Add Media URL" button in asset selection modals and slideouts.

Version 1.0.0

May 4, 2026

Added

  • Initial release.
  • URL detection for 16 media providers (HLS, DASH, Mux, YouTube, Vimeo, Spotify, TikTok, Wistia, JW Player, Twitch, Cloudflare, Shaka, PeerTube, Video.js, MP4, Audio).
  • .pmedia JSON manifest files stored as Craft assets.
  • MediaItemRecord database mirror for fast index queries.
  • Deterministic thumbnail derivation (YouTube, Vimeo, Mux, Cloudflare, Wistia).
  • Custom polymedia file kind with asset index table attributes (Type, Provider, Duration).
  • CP "Add media URL" modal with signed-URL warning.
  • Asset edit sidebar panel with playback defaults.
  • Related assets system (poster, captions, subtitles, descriptions, transcript).
  • VTT validation with BOM stripping.
  • PolymediaField field type (extends Assets) with provider filtering and per-placement poster overrides.
  • Renderer service with player(), element(), data(), is(), scripts().
  • craft.polymedia.* Twig variable API.
  • Media Chrome <media-controller> rendering with correct provider element tags.
  • Script loader modes: CDN, self-host, none.
  • Asset indexer reconciler for orphaned .pmedia files.
  • 102 unit tests, 268 assertions.