Client-side media: Add video transcoding to web-safe formats#79375
Client-side media: Add video transcoding to web-safe formats#79375adamsilverstein wants to merge 8 commits into
Conversation
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.
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 |
There was a problem hiding this comment.
please use the /* */ convention for multiline comments
| // 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, |
There was a problem hiding this comment.
please use the /* */ convention for multiline comments
| // 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 |
There was a problem hiding this comment.
please use the /* */ convention for multiline comments
| // 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 |
There was a problem hiding this comment.
please use the /* */ convention for multiline comments
| // render-time filtering is needed. | ||
| require_once __DIR__ . '/animated-gif-to-video.php'; | ||
|
|
||
| // Video transcoding: clean up the sideloaded web-safe companion when its |
There was a problem hiding this comment.
please use the /* */ convention for multiline comments
| allowedMimeTypes: settings.allowedMimeTypes, | ||
| allImageSizes: settings.allImageSizes, | ||
| bigImageSizeThreshold: settings.bigImageSizeThreshold, | ||
| // Developer opt-out for keeping the original video upload, exposed |
There was a problem hiding this comment.
please use the /* */ convention for multiline comments
| return; | ||
| } | ||
|
|
||
| // Prefer the web-safe transcoded companion when available: the |
There was a problem hiding this comment.
please use the /* */ convention for multiline comments
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +129 kB (+1.49%) Total Size: 8.81 MB 📦 View Changed
|
|
Flaky tests detected in 1cc51f2. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27882031001
|
- 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.
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-conversionpackage, worker boundary, and companion-file infrastructure. It targets that branch and should be rebased ontotrunkonce #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
-scaledand 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 underoptimized_video). Thecore/videoblock points its playbacksrcat the companion; a toolbar control lets authors switch back to the original.Pipeline. Detection and transcoding reuse the
@wordpress/upload-mediaqueue:prepareItemprobes the video metadata (getVideoMetadata, a cheap header read) andneedsVideoTranscodedecides eligibility (non-web-safe container/codec, oversized past the 1920px threshold, or over an optional bitrate budget) — already-optimized small files are skipped.Upload → GenerateVideoCompanion → Finalize; the companion is transcoded off the main thread via mediabunny's high-levelConversionAPI withhardwareAcceleration: 'prefer-hardware', then sideloaded.Developer opt-out. The
gutenberg_video_transcoding_keep_originalPHP filter (defaulttrue) 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
.mov, an.mkv, or a large/high-bitrate.mp4)..mp4once the upload finishes, while the Media Library still holds the original upload..mp4and confirm it is left untouched.add_filter( 'gutenberg_video_transcoding_keep_original', '__return_false' );and confirm only the optimized file is stored.Automated tests
npm run test:unit packages/upload-media packages/video-conversionvendor/bin/phpunit phpunit/media/video-transcoding-test.phpnpm run test:e2e -- test/e2e/specs/editor/various/video-transcoding.spec.jsPart 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.