Skip to content

feat(android): support Media3 1.9 in the load control and re-enable bufferingStrategy - #5099

Merged
mziolkowskii merged 3 commits into
support/6.x.xfrom
fix/android-media3-1.9-load-control
Sep 18, 2026
Merged

mziolkowskii merged 3 commits into
support/6.x.xfrom
fix/android-media3-1.9-load-control

Conversation

@mziolkowskii

@mziolkowskii mziolkowskii commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Stops react-native-video 6 from crashing on Android in apps that resolve Media3 1.9, which is the case with react-native-vision-camera v5, and makes the bufferingStrategy prop work again.

Fixes #4900. Includes #5100, merged into this branch.

Motivation

The crash. RNVLoadControl extended DefaultLoadControl and called its 9-argument protected constructor. Media3 1.9 removed that constructor.

react-native-vision-camera 5 depends on CameraX camera-video:1.7.0-alpha01, which requires media3-muxer/media3-container 1.9.0. Media3's version alignment then resolves media3-exoplayer to 1.9.0 for the whole app, while react-native-video still compiles against its own 1.8.0. The player crashes on creation:

java.lang.NoSuchMethodError: No direct method <init>(Landroidx/media3/exoplayer/upstream/DefaultAllocator;IIIIIZIZ)V in class Landroidx/media3/exoplayer/DefaultLoadControl;
  at com.brentvatne.exoplayer.ReactExoplayerView$RNVLoadControl.<init>(ReactExoplayerView.java:568)
  at com.brentvatne.exoplayer.ReactExoplayerView.initializePlayerCore(ReactExoplayerView.java:724)

The dead prop. bufferingStrategy was implemented in shouldContinueLoading(long, long, float). ExoPlayer has only called shouldContinueLoading(Parameters) since Media3 1.4, so from 6.6.0 on DisableBuffering and DependingOnMemory behaved like Default, and the two bufferConfig fields only that logic reads — maxHeapAllocationPercent and minBufferMemoryReservePercent — had no effect either.

Changes

  • buildLoadControl builds a DefaultLoadControl through DefaultLoadControl.Builder instead of the removed constructor, with the same settings as before: buffer durations and back buffer from bufferConfig, prioritizeTimeOverSizeThresholds = true, and the Builder's defaults for the allocator and targetBufferBytes (identical to the values the old code passed explicitly). The Builder API is unchanged across Media3 1.8, 1.9 and 1.10.
  • RNVLoadControl now implements LoadControl instead of extending DefaultLoadControl. It applies bufferingStrategy in shouldContinueLoading(Parameters) — the overload ExoPlayer actually calls — and forwards everything else to the DefaultLoadControl from buildLoadControl. It forwards only methods whose descriptors are identical in Media3 1.8 through 1.10, and declares both getAllocator() and getAllocator(PlayerId) so it satisfies the interface on either.
  • getAllocator(PlayerId) forwards to DefaultLoadControl by reflection. On 1.9+ DefaultLoadControl counts buffered bytes per player only through the allocator it hands out there, so returning the raw allocator would silently disable its byte limit. The method does not exist in the Media3 this library compiles against, hence the reflection; it is matched by signature so R8 renaming does not break it, and falls back to the raw allocator with a warning.
  • The minimum memory reserve was computed as (long) minBufferMemoryReservePercent * maxMemory, which truncates the 0–1 percentage to 0 before multiplying, so the reserve check never fired. The cast now applies to the product.
  • source and bufferingStrategy are volatile: they are written from the UI thread and, now that the strategy is live, read from the playback thread.

Behavior change

bufferingStrategy starts taking effect again for apps that set it. In particular DisableBuffering now really stops loading, where for the last several minor versions it did nothing. This is why the PR is typed feat rather than fix — it needs a minor bump, not a patch.

shouldContinuePreloading is deliberately not forwarded: it takes a PlayerId in 1.9+ but not in 1.8, so no single declaration compiles against both. ExoPlayer only calls it once a preload configuration is set, which nothing in this library does.

Test plan

The setup from the issue: a new React Native 0.84.0 app (new architecture) with react-native-vision-camera@5.0.9, react-native-nitro-modules@0.35.6 and react-native-nitro-image@0.14.0, rendering a <Video> and a <Camera> on the same screen. Gradle resolves media3-exoplayer to 1.9.0 for the app while react-native-video compiles against 1.8.0. Release build on Android emulators (API 34 and API 37, arm64):

react-native-video Result
6.19.2 from npm crashes with the NoSuchMethodError above, same line numbers as in the issue
this branch no crash; ExoPlayer AndroidXMedia3/1.9.0; camera session and preview start; video loads and onProgress keeps firing

Media3 1.8.0: the same app with every Media3 artifact forced to 1.8.0 (react-native-video's default): this branch plays (AndroidXMedia3/1.8.0, onProgress keeps firing, normal buffering).

Compile and link checks: the load control code compiles against Media3 1.8.0, 1.9.4 and 1.10.1, and for every compile/runtime pair of those versions each Media3 method it references exists with the same descriptor. The same check on the original code reports the missing constructor. LoadControl's only abstract method is getAllocator() in 1.8 and getAllocator(PlayerId) in 1.9+, and declaring both satisfies either runtime.

bufferingStrategy: DisableBuffering and DependingOnMemory were verified to reach shouldContinueLoading(Parameters) again.

Not tested: iOS (not affected), physical camera hardware.

RNVLoadControl extended DefaultLoadControl and called its 9-argument
protected constructor, which Media3 1.9 removed. Apps that resolve Media3 1.9,
for example with react-native-vision-camera v5, which pulls it in through
CameraX, crashed with NoSuchMethodError as soon as a player was created.

Build a plain DefaultLoadControl with its Builder, with the same buffer
settings. The Builder API is the same in Media3 1.8, 1.9 and 1.10.

The subclass only overrode shouldContinueLoading(long, long, float) for
bufferingStrategy, and ExoPlayer has not called that overload since
Media3 1.4, so the override was unreachable and is removed. bufferingStrategy
keeps behaving as it has since 6.6.0; making it work again is a separate change.

Fixes #4900
* fix(android): apply bufferingStrategy again

bufferingStrategy was implemented in DefaultLoadControl's
shouldContinueLoading(long, long, float) overload. ExoPlayer has only called
shouldContinueLoading(Parameters) since Media3 1.4, so from 6.6.0 on
DisableBuffering and DependingOnMemory behaved like Default, and the
bufferConfig fields only that logic reads, maxHeapAllocationPercent and
minBufferMemoryReservePercent, had no effect either.

RNVLoadControl now implements LoadControl, applies the strategy in
shouldContinueLoading(Parameters) and forwards everything else to a
DefaultLoadControl built by buildLoadControl. It forwards only methods whose
signatures are the same in Media3 1.8 through 1.10, and declares both
getAllocator() variants so it compiles against either.

The minimum memory reserve was computed as
(long) minBufferMemoryReservePercent * maxMemory, which casts the 0-1
percentage to 0 before multiplying, so the reserve check never triggered. The
cast now applies to the product.

* fix(android): keep DefaultLoadControl's byte limit on Media3 1.9+

Media3 1.9+ counts buffered bytes per player only through the allocator
DefaultLoadControl returns from getAllocator(PlayerId). Handing out the raw
allocator left that count at zero, so the byte limit never triggered and
only maxBufferMs bounded the buffer.

Forward getAllocator(PlayerId) to DefaultLoadControl through reflection,
resolved once per load control and matched by signature so R8 renaming
does not break it.
@mziolkowskii mziolkowskii changed the title fix(android): support Media3 1.9 in the load control feat(android): support Media3 1.9 in the load control and re-enable bufferingStrategy Sep 16, 2026
…iled allocator lookup

source and bufferingStrategy are written from the UI thread and, now that the
buffering strategy is applied in shouldContinueLoading(Parameters), read from the
playback thread. Mark both volatile so a prop change is not missed there.

getAllocator(PlayerId) is only reached on a runtime that calls the PlayerId
overload, so a null reflection lookup there means DefaultLoadControl's buffered
byte limit is inactive. Log that once instead of falling back silently.

Also name the one LoadControl method the wrapper does not forward:
shouldContinuePreloading takes a PlayerId in 1.9+ but not in 1.8, so no single
declaration compiles against both.
@mziolkowskii
mziolkowskii merged commit ef4f6ef into support/6.x.x Sep 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants