Skip to content

feat(moq-video): add the Android MediaCodec encoder and decoder - #3354

Merged
kixelated merged 3 commits into
moq-dev:mainfrom
Frando:pr/android-mediacodec
Sep 5, 2026
Merged

feat(moq-video): add the Android MediaCodec encoder and decoder#3354
kixelated merged 3 commits into
moq-dev:mainfrom
Frando:pr/android-mediacodec

Conversation

@Frando

@Frando Frando commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Adds Android MediaCodec hardware encode and decode to moq-video, behind the default-on mediacodec feature on Android. H.264 and H.265 encoding use synchronous ByteBuffer input. H.264, H.265, and device-supported AV1 decoding render into an ImageReader, returning AHardwareBuffer surfaces without a CPU round trip.

This PR is part of the work to update iroh-live to the latest moq; see n0-computer/iroh-live#45.

Summary

  • Registers MediaCodec ahead of the software fallback for direct moq-video Android builds.
  • Emits Annex-B access units with in-band parameter sets and supports live bitrate changes.
  • Adds the Android Surface::HardwareBuffer representation, including visible crop geometry and CPU I420 read-back.
  • Adds just rs android so the Android-only source and tests can be cross-compiled locally.

Review repairs

  • Uses Android's actual request-sync parameter key and keeps an IDR request pending until a frame is accepted. The previous key was unknown to MediaCodec and could leave a new group starting with dependent frames.
  • Reads presentation timestamps from each AImage instead of pairing images through a FIFO. Surface frame drops could otherwise shift every later timestamp.
  • Signals decoder end of stream, drains delayed codec output through the ImageReader callback, and flushes the synchronous codec for reuse. This also implements the decoder flush contract added on current main.
  • Applies nonzero crop origins to both CPU plane reads and the public hardware-buffer geometry.
  • Registers the required buffer-removed listener when callers acquire an extra AHardwareBuffer reference.
  • Keeps the API 26 NDK calls behind the mediacodec feature. Direct moq-video Android builds enable it by default, while the language bindings continue to build without default features at Android API 24.

Public API changes

  • On Android with mediacodec, Surface gains the non-exhaustive HardwareBuffer variant.
  • frame::android::HardwareBuffer exposes its owned NDK buffer and visible left, top, width, and height geometry.
  • The matching ndk crate version is re-exported on that target and feature so consumers do not guess an ABI type version.

The language-binding surface is unchanged. Its codec-only build does not enable mediacodec and retains the Android API 24 floor.

Validation

  • nix develop --command just fix
  • nix develop --command just check
  • nix develop --command just test
  • cargo package --locked -p moq-video --allow-dirty --no-verify
  • just rs android with cargo-ndk 4.1.2 and NDK 29, targeting arm64-v8a at API 26
  • cargo ndk -t arm64-v8a --platform 24 check --locked -p moq-video --no-default-features --all-targets

The original branch was also exercised on an x86_64 API 35 emulator through an Android demo application. I did not rerun that emulator test after the review repairs. Physical-device hardware encode, AHardwareBuffer import, and live set_bitrate remain unverified.

(Written by GPT-5)

Frando added a commit to Frando/moq that referenced this pull request Sep 3, 2026
@kixelated
kixelated marked this pull request as ready for review September 5, 2026 00:29
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T02:37:10.361936Z 4959ca2 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 54677988-6120-4e9d-936a-36fb4fac7ffb

📥 Commits

Reviewing files that changed from the base of the PR and between 27c199b and 4959ca2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • doc/lib/rs/crate/moq-video.md
  • doc/lib/rs/index.md
  • rs/justfile
  • rs/moq-video/Cargo.toml
  • rs/moq-video/README.md
  • rs/moq-video/src/decode/backend/mediacodec.rs
  • rs/moq-video/src/decode/backend/mod.rs
  • rs/moq-video/src/decode/decoder.rs
  • rs/moq-video/src/encode/backend/mediacodec.rs
  • rs/moq-video/src/encode/backend/mod.rs
  • rs/moq-video/src/encode/encoder.rs
  • rs/moq-video/src/frame.rs
  • rs/moq-video/src/lib.rs

Included review availability: Your plan provides up to 4 included reviews per hour; 0 remain after this review.


Walkthrough

The change adds Android MediaCodec hardware encoding and decoding for H.264, H.265, and AV1 decoding. It adds Android HardwareBuffer surfaces with crop-aware I420 conversion. The mediacodec feature enables the NDK dependency and becomes a default feature. Backend registration, timestamp handling, flushing, parameter-set handling, bitrate updates, tests, Android build validation, and documentation are included.

Merge Risk: ⚪ Minimal · up to 4959c

No actionable merge-blocking risk is established in the current change.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 8 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: adding Android MediaCodec encoding and decoding support to moq-video.
Description check ✅ Passed The description directly explains the Android MediaCodec encoder, decoder, HardwareBuffer support, feature gating, validation, and known limitations.
Full details: Docstring Coverage

Explanation

Docstring coverage is 72.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 8 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0b85fdf30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-video/src/encode/backend/mediacodec.rs Outdated
Comment thread rs/moq-video/Cargo.toml Outdated
Comment thread rs/moq-video/src/decode/backend/mediacodec.rs Outdated
Frando and others added 3 commits September 4, 2026 19:21
Android is the one platform moq-video had no hardware encoder for, so a
phone fell back to openh264 and spent its battery on a codec the SoC
already implements. This adds MediaCodec in synchronous ByteBuffer mode,
gated on `cfg(target_os = "android")` like the objc2 and windows backend
families, and selectable as `encode::Kind::Named("mediacodec")`.

MediaCodec is a queued device: it encodes frame N while frame N+k goes
in. So each access unit is stamped with the frame it belongs to, found
through the sample time the codec echoes back in its `BufferInfo`, rather
than with whatever frame happens to be going in at the time. That is the
case moq-dev#2503 carried the timestamp through encode for.

Two things the NDK only exposes from API 28, above the API 26 this crate
builds against, are stated in the module header rather than left to be
discovered. The input buffer geometry is unavailable, so NV12 is written
tightly packed and a device whose encoder pads its input rows would shear.
The name of the opened codec is unavailable, so a device with no hardware
encoder gets the AOSP software one under the `mediacodec` name instead of
falling through to openh264, which also means `Kind::Hardware` cannot be
enforced here.

`just rs android` compiles it. Android cross-compiles from the Linux dev
shell in about half a minute, so unlike the `windows` and `macos` recipes
this one needs no special host, only an NDK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The decode counterpart to the MediaCodec encoder, and the surface
vocabulary it needs. A phone was decoding H.264 in openh264 on the CPU
while its SoC had a decoder sitting idle.

The decoder is configured against an `AImageReader` surface, so a decoded
picture never touches system memory: it stays in the graphic buffer the
codec wrote it to, and comes out as the new `Surface::HardwareBuffer`
variant. An application draws it by importing the buffer as an EGLImage in
its own GL context, which is what the Android media stack expects.
`Surface::into_i420` stays total, so a consumer that only wants pixels
reads it back and everything that matches on `Surface` keeps working.

Timestamps take two hops. Output buffers carry their own
`presentation_time_us`, so any reordering the codec does is the codec's
problem. The reader hop is FIFO and carries no timestamp of its own, so a
queue of released timestamps pairs the two up, pushed when a picture is
released for rendering and popped when its image is acquired.

Frame size comes from the image's crop rectangle rather than the
configured format, so a 1080-line picture in a 1088-line buffer comes out
at 1080.

The `Surface::HardwareBuffer` variant lands here rather than on its own,
because a `#[non_exhaustive]` variant with no producer is public API
nothing can reach, and its private constructors would read as dead code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Use Android's actual sync-frame key and retain the request until a frame is accepted. Preserve decoder timestamps and crop origins, drain EOS through the ImageReader, and keep the API 26 backend behind a default feature so API 24 binding builds remain valid.
@kixelated
kixelated force-pushed the pr/android-mediacodec branch from f0b85fd to 4959ca2 Compare September 5, 2026 02:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4959ca2644

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +403 to +409
if self.pending_images > 0 {
tracing::warn!(
decoder = NAME,
dropped = self.pending_images,
"rendered decoder outputs never arrived at the ImageReader"
);
self.pending_images = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not abandon pending images before reusing the reader

When EOS releases more images than can be acquired at once, such as when the caller still owns the reader's slots or the decoder has a tail exceeding QUEUE_DEPTH, collect keeps returning MaxImagesAcquired and this timeout path merely resets the counter. It does not remove the old images from the persistent ImageReader, so after codec.flush() the next stream's initial drain can acquire and return pictures from the previous stream with their old timestamps. Keep the flush pending or recreate/drain the reader before accepting another stream instead of forgetting these images. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Comment on lines +199 to +200
let decoder = media_codec::MediaCodec::from_decoder_type(mime)
.ok_or_else(|| Error::Codec(anyhow::anyhow!("no MediaCodec decoder for {mime}")))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject software codecs for hardware-only selection

On an Android device or emulator that has only a software implementation for the requested MIME type, from_decoder_type can still return that codec. Because this opener is registered in the HARDWARE candidate list, decode::Kind::Hardware then succeeds with a software decoder instead of returning NoDecoder, which is particularly plausible for AV1 and defeats callers relying on the hardware-only CPU and latency guarantee. Query or select a genuinely hardware-accelerated codec before accepting this candidate. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

@kixelated
kixelated merged commit 23f4b48 into moq-dev:main Sep 5, 2026
5 checks passed
@moq-bot moq-bot Bot mentioned this pull request Sep 5, 2026
steelhead99x added a commit to steelhead99x/moq that referenced this pull request Sep 5, 2026
* docs(quest): settle scope narrowing in place, and mark pre-media sidecar placement (moq-dev#3427)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(moq-video): pick the V4L2 mode nearest the requested resolution (moq-dev#3355)

Co-authored-by: Luke Curley <kixelated@gmail.com>
Co-authored-by: Codex <codex@openai.com>

* feat(moq-video): add the Android MediaCodec encoder and decoder (moq-dev#3354)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Luke Curley <kixelated@gmail.com>

* docs(quest): import the post-grooming issues as quests (moq-dev#3431)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* docs(quest): apply the Codex findings on the issue import (moq-dev#3432)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(claude): adopt a quest branch at the remote tip that was inspected (moq-dev#3421)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs(quest): record four findings from the m1 quest wave (moq-dev#3424)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: reorganize the site around what a reader can do (moq-dev#3426)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* chore: ignore Claude Code's scratch directories (moq-dev#3428)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(moq-audio,moq-cli): assert publish_capture stays Send off macOS (moq-dev#3433)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: correct claims found during merge review (moq-dev#3435)

Co-authored-by: GPT-5 <noreply@openai.com>

* docs(quest): import the open issues that had no quest, and gate the dev merge (moq-dev#3434)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* docs(moq-audio): scope the local-task guidance to macOS (moq-dev#3436)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: track deferred review findings (moq-dev#3438)

Co-authored-by: GPT-5 <noreply@openai.com>

* chore: remove redundant packaging work and plan relay ownership fixes (moq-dev#3440)

Co-authored-by: GPT-6 <noreply@openai.com>

* perf(net): avoid redundant chunk copies and plan performance investigations (moq-dev#3443)

Co-authored-by: GPT-6 <noreply@openai.com>

* fix(transcode): follow a source resolution change with the ladder (moq-dev#3381)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: GPT-5 <noreply@openai.com>

* feat(watch): share one AudioContext across audio decoders

Spatial playback needs every remote in the same Web Audio graph. Injected
contexts are never closed.

Co-Authored-By: Cursor Grok 4.6 <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Luke Curley <kixelated@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Franz Heinzmann <frando@unbiskant.org>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: GPT-5 <noreply@openai.com>
Co-authored-by: Cursor Grok 4.6 <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants