Skip to content

Client-side media: convert HEIC/HEIF image sequences (Live Photo / burst) to video - #79647

Open
adamsilverstein wants to merge 5 commits into
trunkfrom
add/heic-sequence-to-video
Open

Client-side media: convert HEIC/HEIF image sequences (Live Photo / burst) to video#79647
adamsilverstein wants to merge 5 commits into
trunkfrom
add/heic-sequence-to-video

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 29, 2026

Copy link
Copy Markdown
Member

What

Adds client-side support for HEIC/HEIF image sequences — Apple Live Photos (HEVC sequences) and Android bursts (image/heic-sequence, image/heif-sequence; .heics/.heifs) — by converting them to a web-safe video (MP4/WebM) in the browser before upload.

Resolves part of #79642.

Today these files cannot be handled at all: GD/Imagick decode only a single frame, so WordPress core deliberately collapses sequence uploads to one still frame (see Trac #65297 / wordpress-develop#12352). Client-side conversion is the only path that preserves the motion.

Note

Builds on #78410 (Animated GIF → video via mediabunny), now merged to trunk. The video encoding reuses the @wordpress/video-conversion worker introduced there.

How

The existing still-HEIC pipeline only parses the meta/iloc/pitm structure of a single image. Image sequences instead store their frames as samples of an ISOBMFF movie track (moovtrakmdiaminfstbl), so the work splits into three pieces:

  1. Demux (packages/upload-media/src/heic-parser.ts): new parseHeicSequence() walks the sample tables (stsd/hvcC, stsz, stsc, stco/co64, stts, stss) and extracts the temporal HEVC access units, their durations, and keyframe flags. Reuses the existing Reader/buildCodecString helpers.
  2. Decode + encode (@wordpress/video-conversion): the GIF encode loop is refactored into a shared encodeFramesToVideo helper, and convertHeicSequenceToVideo decodes the frames with the WebCodecs VideoDecoder (platform HEVC codec) and re-encodes them with mediabunny — exactly like the GIF path, but sourced from a VideoDecoder instead of an ImageDecoder. Decoding is pipelined with a bounded look-ahead (8 frames) so a long sequence never materializes all of its full-res frames at once.
  3. Route (packages/upload-media/src/store/private-actions.ts): prepareItem detects the sequence MIME types and converts them to video before upload. The video becomes the attachment. When WebCodecs is unavailable it falls through to upload the original file (the server collapses it to a still).

Parsing happens on the main thread (keeping heic-parser in @wordpress/upload-media); the heavy decode + encode runs in the worker, off the main thread.

flowchart LR
    A[".heics / .heifs"] --> B["parseHeicSequence()<br/>(main thread)"]
    B -->|HEVC samples| C["VideoDecoder<br/>(worker)"]
    C -->|VideoFrames| D["mediabunny encode<br/>(worker)"]
    D --> E["MP4 / WebM"]
    E --> F["upload as video attachment"]
Loading

Prior art researched

  • swissspidy's media-experiments plugin recognizes the sequence brands (msf1/hevc/hevx) but decodes only the first frame via libheif-js and discards the rest; its only image→video path is GIF→video via ffmpeg.wasm. No sequence→video support to reuse.
  • mediabunny is an encoder/muxer; it does not demux HEIF sequences, but its encode side (VideoSampleSource + Output + Mp4OutputFormat/WebMOutputFormat) is exactly what this reuses. The demuxer is the net-new piece.

Testing instructions

  • npm run test:unit packages/upload-media packages/video-conversion
  • The demuxer is unit-tested against a real 120-frame msf1.heic fixture (256×144, 25fps, 1 keyframe + 119 delta frames), verifying frame count, codec config, sync/delta flags, and per-frame timing.
  • Manual: upload an iPhone Live Photo exported as .heics (or an Android burst) into the editor with client-side media enabled; it should upload as a short looping MP4/WebM.

Open questions / follow-ups (from #79642)

  • UX: the result is currently a plain video attachment. Whether a Live Photo should become a looping <video> (e.g. the GIF block variation) vs. a still-with-motion is left as a follow-up.
  • Original preservation: the original .heics is not yet sideloaded alongside the video (the motion is preserved in the video itself).
  • B-frame reordering: the look-ahead window handles typical sequences (the common case, and the test fixture, have no ctts); deeply reordered streams would need composition-time handling.

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

@adamsilverstein
adamsilverstein marked this pull request as draft June 29, 2026 18:19
@adamsilverstein adamsilverstein added [Feature] Client Side Media Media processing in the browser with WASM [Type] Feature New feature to highlight in changelogs. [Status] In Progress Tracking issues with work in progress labels Jun 29, 2026
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Size Change: +2.03 kB (+0.03%)

Total Size: 7.68 MB

📦 View Changed
Filename Size Change
build/modules/video-conversion/worker.min.js 83.7 kB +793 B (+0.96%)
build/scripts/upload-media/index.min.js 15.6 kB +1.23 kB (+8.57%) 🔍

compressed-size-action

Base automatically changed from add/gif-to-video-mediabunny to trunk June 29, 2026 18:44
@adamsilverstein
adamsilverstein force-pushed the add/heic-sequence-to-video branch 2 times, most recently from 7f7f045 to a5c4ac7 Compare June 30, 2026 15:50
Add parseHeicSequence() to extract the temporal HEVC frames from an
image-sequence file (msf1 brand — Apple Live Photo HEVC sequences and
Android bursts). Unlike still HEIC, sequences store frames as samples of
a track in the moov/mdat boxes, so this demuxes the stbl sample tables
(stsd/hvcC, stsz, stsc, stco/co64, stts, stss) into individual access
units ready for the WebCodecs VideoDecoder.

Tested against a real 120-frame 256x144 25fps msf1 fixture.
Refactor the mediabunny encode loop into a shared encodeFramesToVideo helper
and add convertHeicSequenceToVideo: it decodes the demuxed HEVC frames with
the WebCodecs VideoDecoder (pipelined with a bounded look-ahead so a long
sequence never holds all its full-res frames at once) and re-encodes them to
MP4/WebM, exactly like the GIF path but sourced from a VideoDecoder instead of
an ImageDecoder. Add a thin upload-media wrapper that demuxes on the main
thread (keeping heic-parser in upload-media) and hands the samples to the
worker. Reuses the GIF path's Unsupported-error fallback contract.
Detect image/heic-sequence and image/heif-sequence in prepareItem and convert
them to a web-safe video before upload (the server cannot decode multi-frame
HEVC sequences). The video is uploaded as the attachment. When WebCodecs is
unavailable, fall through to upload the original file so the server can still
collapse it to a single still frame.
Add prepareItem tests covering the sequence-to-video routing (convert and
upload, unsupported fall-through, hard-failure cancel, WebCodecs gate) and
document the new behavior in the upload-media and video-conversion changelogs.
@adamsilverstein
adamsilverstein force-pushed the add/heic-sequence-to-video branch from a5c4ac7 to 0b27ad8 Compare June 30, 2026 16:02
@github-actions

Copy link
Copy Markdown

Flaky tests detected in 0b27ad8.
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/28458230046
📝 Reported issues:

…-video

# Conflicts:
#	packages/upload-media/CHANGELOG.md
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 [Status] In Progress Tracking issues with work in progress [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant