Skip to content

Client-side media: Add video transcoding to web-safe formats#79375

Open
adamsilverstein wants to merge 8 commits into
add/gif-to-video-mediabunnyfrom
add/video-transcoding-mediabunny
Open

Client-side media: Add video transcoding to web-safe formats#79375
adamsilverstein wants to merge 8 commits into
add/gif-to-video-mediabunnyfrom
add/video-transcoding-mediabunny

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 20, 2026

Copy link
Copy Markdown
Member

What?

Adds client-side video transcoding to web-safe formats for the client-side media feature, implementing #79363. When a video that is not already web-safe is uploaded, it is transcoded in the browser (via mediabunny / WebCodecs) to MP4/H.264 (or WebM/VP9) before it is served, mirroring what we already do for images and the GIF-to-video conversion in #78410.

Note

This PR is stacked on #78410 (add/gif-to-video-mediabunny) and reuses its @wordpress/video-conversion package, worker boundary, and companion-file infrastructure. It targets that branch and should be rebased onto trunk once #78410 lands. Review the last 6 commits.

Why?

WordPress accepts video uploads in any format (.mov, .mkv, high-bitrate .mp4) and never transcodes them, because server-side transcoding needs FFmpeg, which most hosts lack. The result is slow page loads, high bandwidth/storage cost, and clips that won't play in some browsers. This is one of the capabilities called out for the 7.1 cycle and was only a roadmap bullet on #76756.

How?

Keep the original, serve a web-safe companion. WordPress always preserves the original upload (so it can be linked to or used to regenerate later, like the -scaled and HEIC originals). So the original video is stored as the attachment and the transcoded, web-safe version is sideloaded as a companion file (recorded in attachment metadata under optimized_video). The core/video block points its playback src at the companion; a toolbar control lets authors switch back to the original.

Pipeline. Detection and transcoding reuse the @wordpress/upload-media queue:

  • prepareItem probes the video metadata (getVideoMetadata, a cheap header read) and needsVideoTranscode decides eligibility (non-web-safe container/codec, oversized past the 1920px threshold, or over an optional bitrate budget) — already-optimized small files are skipped.
  • Eligible videos get Upload → GenerateVideoCompanion → Finalize; the companion is transcoded off the main thread via mediabunny's high-level Conversion API with hardwareAcceleration: 'prefer-hardware', then sideloaded.
  • Video encoding shares the single-concurrency WebCodecs gate with GIF conversion.
  • Graceful fallback: if the browser cannot encode, the companion is silently skipped (original stays); in replace-primary mode the original is uploaded unchanged so the user never loses their file.

Developer opt-out. The gutenberg_video_transcoding_keep_original PHP filter (default true) flips the pipeline to transcode-before-upload, storing only the optimized file. Since video files can be very large, this affords the ability to reduce storage requirements with the tradeoff that the original video is no longer available for download or regeneration.

Testing Instructions

  1. Enable client-side media processing and open the editor in a Chromium 137+ browser.
  2. Add a Video block and upload a non-web-safe video (e.g. a .mov, an .mkv, or a large/high-bitrate .mp4).
  3. Confirm the block plays a transcoded .mp4 once the upload finishes, while the Media Library still holds the original upload.
  4. Use the "Use original video" / "Use optimized video" toolbar toggle to switch the playback source.
  5. Upload an already-web-safe, small .mp4 and confirm it is left untouched.
  6. Add add_filter( 'gutenberg_video_transcoding_keep_original', '__return_false' ); and confirm only the optimized file is stored.

Automated tests

  • Unit: npm run test:unit packages/upload-media packages/video-conversion
  • PHP: vendor/bin/phpunit phpunit/media/video-transcoding-test.php
  • e2e: npm run test:e2e -- test/e2e/specs/editor/various/video-transcoding.spec.js

Part of #76756.

AI use

This PR was written by Claude after careful prompting, planning and review from me. I also plan to manually review the code and test the feature directly.

Detect uploaded videos that are not web-safe (non-MP4/WebM container,
non-web-safe codec, oversized, or over a bitrate budget) and transcode
them to MP4/H.264 (or WebM/VP9) in the browser before they are served,
mirroring the GIF-to-video flow.

The original upload is preserved as the attachment (consistent with how
WordPress keeps the `-scaled` and HEIC originals); the transcoded,
web-safe version is sideloaded as a companion file and is what plays.
A `videoKeepOriginal` setting flips the pipeline to replace-primary so
only the optimized file is stored.

Transcoding runs off the main thread in the existing
@wordpress/video-conversion worker via mediabunny's Conversion API, with
hardware acceleration where available and graceful fallback to the
original when the browser cannot encode. Video encoding shares the
single-concurrency WebCodecs gate with GIF conversion.

Part of #76756. Implements #79363.
Add server-side support for the `optimized-video` sideload size: the
transcoded, web-safe video is stored as a companion file recorded in
attachment metadata under `optimized_video` (surfaced to the editor via
media_details), the original stays the attachment, and the companion is
cleaned up on delete_attachment.

Add the `gutenberg_video_transcoding_keep_original` filter (default
true). When a developer returns false, the resolved value is exposed as
window.__videoTranscodingKeepOriginal and read by the block editor's
media upload settings, flipping the pipeline to transcode-before-upload
so only the optimized file is stored.

Part of #76756. Implements #79363.
When a video with an `optimized_video` companion is uploaded or selected,
the Video block now plays the web-safe transcoded version (its `src`
points at the companion) while the attachment keeps pointing at the
original. A new toolbar control lets the author toggle between the
optimized companion and the original upload.

Part of #76756. Implements #79363.
Cover the worker transcodeVideo/getVideoMetadata functions (success,
codec selection, downscaling, and the Unsupported graceful-failure
paths), the prepareItem decision logic (companion vs replace-primary vs
skip-already-optimized), the transcodeVideoItem handler's two graceful
fallbacks (silent companion cancel vs upload-original), the
generateVideoCompanion sideload, and the shared video-processing
concurrency selectors.

Part of #76756. Implements #79363.
PHP: cover the optimized_video companion path resolution (including
directory-traversal hardening), delete_attachment cleanup, and the
gutenberg_video_transcoding_keep_original filter default + override.

e2e: upload a browser-generated WebM into a Video block, assert the
block plays the transcoded MP4 companion while the original WebM stays
the attachment (with optimized_video in its metadata), and that the
toolbar toggles back to the original. The fixture is generated at
runtime via MediaRecorder, so no binary asset is committed.

Part of #76756. Implements #79363.
@github-actions github-actions Bot added [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor labels Jun 20, 2026
@adamsilverstein adamsilverstein added [Feature] Client Side Media Media processing in the browser with WASM [Status] In Progress Tracking issues with work in progress [Type] Enhancement A suggestion for improvement. labels Jun 20, 2026
The inferred return type referenced VideoMetadata from the
@wordpress/video-conversion worker package, which TypeScript flagged as
non-portable (TS2883) during the project build. Annotate the wrapper
with an explicit structural return type that mirrors the worker's
VideoMetadata shape.
// See lib/media/animated-gif-to-video.php.
$metadata['animated_video_poster'] = $sub_size['file'];
} elseif ( 'optimized-video' === $image_size ) {
// Web-safe transcoded companion of an uploaded video. Stored

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the /* */ convention for multiline comments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22e3520.

// dimension checks (the caller passes (0, 0) for this case so the
// positive-dimension assertion below would otherwise fire).
if ( 'animated-video' === $image_size ) {
// 'animated-video' and 'optimized-video' companion files are videos,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the /* */ convention for multiline comments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22e3520.

// upload as "corrupted or unsupported". Skip the read for this case;
// validate_image_dimensions() also short-circuits it below.
$size = 'animated-video' === $image_size ? array( 0, 0 ) : wp_getimagesize( $path );
// 'animated-video' and 'optimized-video' companions are video files

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the /* */ convention for multiline comments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22e3520.

// video block's poster and deleted with the video.
$sub_size_data['file'] = wp_basename( $path );
} elseif ( 'optimized-video' === $image_size ) {
// Web-safe transcoded video companion. finalize_item() writes the

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the /* */ convention for multiline comments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22e3520.

Comment thread lib/media/load.php Outdated
// render-time filtering is needed.
require_once __DIR__ . '/animated-gif-to-video.php';

// Video transcoding: clean up the sideloaded web-safe companion when its

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the /* */ convention for multiline comments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22e3520.

allowedMimeTypes: settings.allowedMimeTypes,
allImageSizes: settings.allImageSizes,
bigImageSizeThreshold: settings.bigImageSizeThreshold,
// Developer opt-out for keeping the original video upload, exposed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the /* */ convention for multiline comments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22e3520.

return;
}

// Prefer the web-safe transcoded companion when available: the

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the /* */ convention for multiline comments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22e3520.

@adamsilverstein adamsilverstein self-assigned this Jun 20, 2026
@adamsilverstein adamsilverstein marked this pull request as ready for review June 20, 2026 19:57
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

Size Change: +129 kB (+1.49%)

Total Size: 8.81 MB

📦 View Changed
Filename Size Change
build/modules/video-conversion/worker.min.js 209 kB +128 kB (+159.77%) 🆘
build/scripts/block-editor/index.min.js 380 kB +39 B (+0.01%)
build/scripts/block-library/index.min.js 325 kB +290 B (+0.09%)
build/scripts/upload-media/index.min.js 14.7 kB +559 B (+3.95%)

compressed-size-action

@github-actions

Copy link
Copy Markdown

Flaky tests detected in 1cc51f2.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27882031001
📝 Reported issues:

- Use the /* */ block convention for multi-line comments.
- Delete the optimized_video companion via wp_delete_file_from_directory
  confined to the uploads directory, matching the source_image (HEIC)
  companion cleanup; trust only the basename of the recorded value.
- Regenerate the @wordpress/video-conversion README API docs.
Add a small (6 KB) WebM clip under test/e2e/assets and upload it in the
video transcoding e2e test, replacing the runtime MediaRecorder
generation. With the default MP4 output target the WebM is a real
non-web-safe input that exercises the actual transcode to an MP4
companion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Client Side Media Media processing in the browser with WASM [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Status] In Progress Tracking issues with work in progress [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant