Miruro Playback Fails When Google Play Services Is Disabled (Cronet/GMS Dependency) #2

Open
opened 2026-07-04 21:14:08 +00:00 by k-nacion · 0 comments
Owner

Miruro Playback Fails When Google Play Services Is Disabled (Cronet/GMS Dependency)

Header

Field Value
Date 2026-07-03
Reporter User (emulator testing)
Platform Android emulator (Google Play Services toggled off/on)
Component HTTP engine (AnimeApi.initHttpEngine), Miruro source (SD8)
HTTP client OkHttpClient (default TLS) + Cronet (fallback)
Status IN PROGRESS — findings revised (2026-07-05 device test corrected the earlier "RESOLVED" conclusion; see "Device Test — 2026-07-05" below)

| Severity | Critical — Miruro is unplayable on all stock Fire TV / Firestick devices (Fire OS has no Google Play Services) plus any non-GMS / GMS-disabled Android device |


Summary

Miruro videos stop playing after Google Play Services is disabled, and start
playing again once it is re-enabled. The app relies on Cronet (Chromium's
network stack)
to produce a Chrome-like TLS fingerprint for Miruro's
Cloudflare-protected CDNs. Cronet is loaded from Google Play Services
(play-services-cronet, bundled via implementation), while the offline
fallback (cronet-embedded) is declared compileOnly and therefore is
NOT packaged in the APK. When Play Services is disabled, no Cronet engine
can initialize, so Miruro requests fall back to OkHttp / the platform HTTP
stack, whose TLS fingerprint Cloudflare rejects (HTTP 400/403) — breaking
playback.


Key Findings

1. Miruro requires Cronet's TLS fingerprint

Miruro's secure pipe and its CDNs sit behind Cloudflare, which rejects
non-Chrome TLS fingerprints. The code documents this explicitly:

AnimeApi.java (Http constructor, ~line 722):

/* forceCronet routes the request through Cronet's HttpURLConnection
 * regardless of Conf.HTTP_CLIENT. This mirrors the Electron fix that
 * routes Cloudflare TLS-strict endpoints (e.g. the Miruro secure pipe)
 * through Chromium's network stack. OkHttp's TLS fingerprint is rejected
 * by Cloudflare with HTTP 400, so when forceCronet is true we never fall
 * back to OkHttp. */

2. Cronet is loaded from Google Play Services (the dependency at risk)

app/build.gradle:

implementation 'com.google.android.gms:play-services-cronet:18.1.0'
// ...
compileOnly 'org.chromium.net:cronet-embedded:119.6045.31'
  • play-services-cronet is implementation → the Cronet provider is
    bundled, but the actual Cronet engine is delivered at runtime by Google
    Play Services
    . If Play Services is disabled/absent, this provider has
    nothing to load.
  • cronet-embedded (the self-contained native fallback that would NOT need
    GMS) is compileOnly → its native .so is intentionally excluded from
    the APK to keep size down.

3. Both Cronet init attempts fail without GMS

AnimeApi.initHttpEngine():

Attempt 1 — auto-discovery relies on the Play Services provider:

CronetEngine.Builder myBuilder = new CronetEngine.Builder(c) ... ;
cronetClient = myBuilder.build();          // fails when GMS disabled

Attempt 2 — explicit embedded native provider:

org.chromium.net.impl.NativeCronetProvider nativeProvider =
    new org.chromium.net.impl.NativeCronetProvider(c);   // NoClassDefFoundError
...
} catch (Throwable embeddedException) {
    // Catches NoClassDefFoundError when cronet-embedded is compileOnly
    Log.e(_TAG,"Cronet Init FAILED completely. CF-protected CDNs will return 403.",embeddedException);
    cronetClient = null;
}

Because cronet-embedded is compileOnly, the native classes are missing at
runtime → NoClassDefFoundError → cronetClient == null.

4. Fallback path reproduces the Cloudflare rejection

With cronetClient == null, a forceCronet request degrades to the platform
HttpURLConnection:

if (forceCronet){
  Log.e(_TAG,"forceCronet FALLBACK to platform HttpURLConnection — "
      +"Cronet unavailable; Cloudflare-protected requests will likely 400");
}
URL netConn = new URL(url);
http = (HttpURLConnection) netConn.openConnection();

The platform/OkHttp TLS fingerprint is rejected by Cloudflare (HTTP 400/403),
so the Miruro stream/pipe never resolves → no playback.

5. The HTTP_CLIENT settings toggle is bypassed for Miruro CDNs

User tested switching the app's HTTP client setting to "generic HTTP client"
and saw identical broken behavior while GMS was disabled. This is expected
and further confirms the root cause: for Miruro's Cloudflare-protected CDNs, the
DataSource selection ignores the user's HTTP_CLIENT setting and computes
its own effectiveHttpClient.

AnimeView.java (video DataSource factory, ~line 1088):

int effectiveHttpClient = Conf.HTTP_CLIENT;
String urlLower = videoStatCurrentUrl.toLowerCase();
if (urlLower.contains("mewstream.buzz") || urlLower.contains("nekostream.site")
    || urlLower.contains("ultracloud.cc") || urlLower.contains("krussdomi.com")
    || urlLower.contains("lostproject.club") || urlLower.contains("anizara.store")
    || urlLower.contains("cloudbuzz.lol") || urlLower.contains("trycloud.pro")) {
  if (AnimeApi.cronetClient != null) {
    effectiveHttpClient = 2; /* Cronet — Chrome TLS fingerprint passes CF */
  } else if (AnimeApi.httpClient != null) {
    effectiveHttpClient = 0; /* OkHttp fallback if Cronet unavailable */
  }
}

Consequence table (GMS disabled → cronetClient == null):

User setting What actually runs for Miruro CDN Result
0 (OkHttp) OkHttp (forced fallback) ❌ CF 403
1 (Generic) OkHttp (forced fallback, setting ignored) ❌ CF 403
2 (Cronet) OkHttp (Cronet null, forced fallback) ❌ CF 403

So no HTTP_CLIENT choice can fix Miruro while GMS is disabled — only a working
Cronet engine can. This is precisely why the "generic HTTP client" test showed
the same behavior. It also means the only setting that can differ is when GMS
is enabled and Cronet exists: then Miruro is forced onto Cronet regardless
of the toggle.

6. Why re-enabling GMS "fixes" it

Re-enabling Play Services restores the Cronet provider → Attempt 1 succeeds →
cronetClient != null → Miruro requests use the Chrome-like TLS fingerprint →
Cloudflare accepts them → playback works again. This exactly matches the
reported off → broken, on → working behavior.

7. Confirming logcat evidence (GMS disabled)

Captured while GMS was disabled and the HTTP client was switched to "generic",
which triggered a setHttpClient → initHttpEngine re-init:

E GooglePlayServicesUtil: GooglePlayServices not available due to error 3
I PlayServicesCronet: Google-Play-Services-Cronet-Provider is not installed yet.
W ATVLOG-API: Cronet primary init failed: All available Cronet providers are disabled.
E ATVLOG-API: Cronet Init FAILED completely. CF-protected CDNs will return 403.
E ATVLOG-API: java.lang.NoClassDefFoundError: Failed resolution of:
              Lorg/chromium/net/impl/NativeCronetProvider;
   Caused by: java.lang.ClassNotFoundException: Didn't find class
              "org.chromium.net.impl.NativeCronetProvider" on path: DexPathList[...]
   at com.amarullz.androidtv.animetvjmto.AnimeApi.initHttpEngine(AnimeApi.java:658)

This is the exact two-attempt failure chain the analysis predicted:

  1. Attempt 1 (Play Services Cronet) fails — GooglePlayServices not available due to error 3 (error code 3 = SERVICE_DISABLED) → "All available Cronet
    providers are disabled."
  2. Attempt 2 (embedded native) fails — NoClassDefFoundError /
    ClassNotFoundException for org.chromium.net.impl.NativeCronetProvider,
    because the compileOnly cronet-embedded native lib is not present in
    the APK's DexPathList
    . This is the direct proof of Finding #2/#3.
  3. Result — cronetClient == null → "CF-protected CDNs will return 403."

Secondary (non-root-cause) noise: the java.net.SocketException: Socket closed and java.io.IOException: Canceled entries occur because switching the
HTTP client setting calls AnimeApi.initHttpEngine(), which runs
dispatcher().cancelAll() and shuts down the OkHttp executor — cancelling the
in-flight home-feed requests (aniwave/miruro trending) that were mid-TLS
handshake. These are a benign side effect of the live re-init, not the Miruro
playback failure.

8. Visible symptom (user-facing)

With GMS disabled, playing a Miruro title shows an indefinite "LOADING DATA"
spinner on a black screen with no playback controls
(see reporter screenshot,
Pixel_4_XL emulator). The stream URL never resolves (Cloudflare rejects the
non-Cronet requests), so ExoPlayer never receives a playable manifest → the
player UI (menu / playback buttons) is never built and the spinner never
clears.

With GMS enabled, the same title loads the manifest, shows the normal playback
menu buttons, and plays — matching the reporter's description.

This confirms the failure is at stream resolution, before ExoPlayer can
render anything, which is exactly what a null cronetClient + Cloudflare 400/403
produces.

9. Version regression timeline (git-traced)

Traced via app/build.gradle history across the 5.x tags:

Version play-services-cronet (GMS) Cronet DataSource routing compileOnly cronet-embedded fallback
5.13.0 ✅ present ❌ not yet ❌ absent
5.14.0 ✅ present ✅ added (819a6644) ❌ absent
5.14.1 ✅ present ✅ present ❌ absent
5.14.2 ✅ present ✅ present ✅ added (compileOnly)
5.14.5 (HEAD) ✅ present ✅ present ✅ present (compileOnly)

Conclusion on "was this introduced after 5.14.1?": No.

  • The behavior that makes Miruro playback depend on Cronet (and thus GMS) was
    introduced in v5.14.0 via commit 819a6644
    ("feat: implement triple DataSource switching (OkHttp/Generic/Cronet)"),
    which added the media3 Cronet DataSource and forced Miruro CDNs onto Cronet.
    This is at/before 5.14.1 — so 5.14.1 is already affected.
  • The compileOnly cronet-embedded line was added later, in v5.14.2. Because
    compileOnly does not package the native library, it never provided a real
    offline fallback; it only causes the second init attempt to fail with
    NoClassDefFoundError. So 5.14.2+ did not fix (nor introduce) the core issue —
    it merely added a non-functional fallback attempt.
  • play-services-cronet itself predates all of this (present since ≥ 5.13.0),
    but was harmless until 5.14.0 wired Miruro playback through Cronet.

Test Result — Cronet-forcing Disabled (2026-07-04)

To validate the root cause, both Cronet-forcing layers for Miruro were
temporarily disabled behind TEST-ONLY toggles (on branch
experiment/miruro-cronet-play-services-fix), so the user's HTTP_CLIENT
setting is honored instead of being overridden to Cronet:

  1. AnimeView.java video DataSource — FORCE_CRONET_FOR_MIRURO_CDN = false
    (skips the CF-CDN override in the videoDataSourceFactory).
  2. AnimeView.java Miruro secure pipe — MIRURO_PIPE_FORCE_CRONET = false
    (the pipe fetch that resolves the stream URL was independently forcing
    Cronet; it now honors Conf.HTTP_CLIENT too).

Result: With GMS disabled and Cronet-forcing off, Miruro plays
using the non-Cronet HTTP client. The build compiled cleanly and playback
worked on the emulator without Google Play Services.

What this changes about the root cause

The original Findings #4–#6 predicted that any non-Cronet client would be
rejected by Cloudflare (400/403), implying only a working Cronet engine could
play Miruro. The test disproves the absolute form of that claim: at least
for the CDN/pipe paths exercised in this test, OkHttp / the platform client
can reach Miruro without Cronet.

Revised understanding: the GMS-disabled failure was caused primarily by the
forced-Cronet routing degrading to a broken fallback, not by an inherent
Cloudflare requirement for a Chrome TLS fingerprint on every Miruro request.
When the code forces Cronet and Cronet is null, the fallback path (and/or its
header/config assumptions) fails — whereas letting OkHttp handle the request
normally succeeds.

Implication for the fix

This reopens a cheaper alternative to shipping cronet-embedded
(which the user has confirmed is undesirable due to the ~20–30 MB APK size
increase): instead of bundling Cronet, stop force-routing Miruro CDNs through
Cronet when cronetClient == null
and let the existing OkHttp/platform
client handle them. This avoids the GMS dependency and avoids the APK bloat.

Still pending: the user is testing all Miruro providers (ally, bee, pewe,
bonk, hop, kiwi, etc.) with Cronet-forcing disabled to confirm none of them
actually require the Chrome TLS fingerprint. If some providers still need
Cronet, a targeted per-provider decision (rather than a blanket force) would be
the correct fix.


Final Resolution (2026-07-05) — SUPERSEDED (see "Device Test — 2026-07-05" below)

⚠️ This "RESOLVED" section was written before the 2026-07-05 on-device test
on a real GMS-disabled Android TV box. That test contradicts the assumptions
here: with the setting honored, "Generic HttpClient" and "OkHttp" behaved
DIFFERENTLY (Generic = stuck loading, OkHttp = plays). The conclusions in this
section are retained for history but are NOT the final resolution. Read
"Device Test — 2026-07-05" for the corrected findings.

Fix implemented (Option A): honor the user's HTTP_CLIENT setting; force
Cronet only for the Kiwi CDN when a Cronet engine exists.

Following the 2026-07-04 test that proved Miruro plays over OkHttp/platform
without Cronet, the two blanket Cronet-forcing layers were removed and replaced
with the user's setting. Testing across Miruro providers confirmed only the
Kiwi CDN (owocdn.top, uwucdn.top, nekostream.site, kwik.cx) still
needs Cronet's Chrome TLS fingerprint. This narrow exception is applied only
when AnimeApi.cronetClient != null
; otherwise it falls back to the user's
HTTP_CLIENT setting (no broken forced-null path).

Changes (in AnimeView.java)

  1. Video DataSource factory (~line 1088): removed the blanket CF-CDN Cronet
    override. effectiveHttpClient now starts from Conf.HTTP_CLIENT and is only
    bumped to Cronet (2) for Kiwi URLs if a Cronet engine is available; if
    Kiwi is detected but Cronet is null, it logs and falls back to the setting.
  2. Miruro secure pipe (~line 2837): the pipe fetch now uses
    new AnimeApi.Http(pipeUrl, false) (forceCronet=false), honoring the setting.
    Browser-like headers + WebView cookie forwarding remain to satisfy Cloudflare.

Outcome

  • Miruro plays on GMS-disabled / Fire TV devices using OkHttp/platform.
  • The Kiwi CDN still gets Cronet when GMS is present, preserving its playback.
  • No APK bloat — cronet-embedded remains compileOnly; we did not bundle
    the embedded Chromium net stack.

Source isolation note

Per the source-isolation rule, these changes affect only the Miruro (SD7/SD8)
paths and the Kiwi exception within them; no other source's CDN resolution was
touched.


Device Test — 2026-07-05 (real GMS-disabled Android TV box) — CORRECTED FINDINGS

Setup: A physical Android TV box at 192.168.100.106 with Google Play
Services disabled
, deliberately configured to emulate a stock Fire TV /
Firestick (no GMS). A debug build was deployed via ./deploy.sh. The deployed
AnimeView.java was the TEST-TOGGLE variant — both Cronet-forces disabled
(FORCE_CRONET_FOR_MIRURO_CDN = false, MIRURO_PIPE_FORCE_CRONET = false) — so
the user's HTTP_CLIENT setting is fully honored for every Miruro request (no
Cronet forcing anywhere).

Results (Miruro playback, GMS disabled):

HTTP_CLIENT setting Engine actually used Miruro result
0 — OkHttp OkHttp + DoH ✅ Plays
1 — Generic Platform HttpURLConnection ❌ Stuck on "LOADING DATA"
2 — Cronet Cronet → unavailable (no GMS) ❌ Fails (engine never inits)

This corrects the earlier conclusions. The 2026-07-04 test and the
"RESOLVED" section assumed OkHttp and Generic behave the same. They do not:

  • OkHttp works on the GMS-disabled box. OkHttp's TLS fingerprint + DoH is
    accepted by Cloudflare on the Miruro pipe/CDNs.
  • Generic does NOT work. The plain platform HttpURLConnection has a
    different (plain-JDK) TLS fingerprint and no DoH, and Cloudflare does not
    accept it for the Miruro pipe → the stream URL never resolves → indefinite
    "LOADING DATA".
  • Cronet cannot even initialize without GMS (see Findings #2/#3).

Revised root-cause conclusion

The original IA premise — "Miruro requires Cronet" — is wrong for GMS-less
devices
. On a real Fire-TV-like device, OkHttp is the ONLY client that both
(a) initializes without GMS and (b) passes Cloudflare.
Forcing Cronet on such
devices is the worst option (it is unavailable), and Generic is the weakest
(no DoH, non-Chrome TLS).

Corrected fix direction (proposed, not yet implemented)

For the Miruro secure pipe and CF-protected CDNs, the correct behavior on
GMS-less devices is to force OkHttp (not Cronet, not Generic) — i.e. prefer
AnimeApi.httpClient when cronetClient == null, rather than degrading to the
platform HttpURLConnection or honoring a Generic setting that Cloudflare
rejects. This keeps Miruro playable on Fire TV without APK bloat.


Regression History — Where It Broke and Where It Worked (git-traced)

This section preserves the exact commit history so the regression is not lost.
All commits are relative to tag v5.12.0 (commit 1ddafb57, dated
2026-05-26), because every relevant commit sits after 5.12.0 (they all
git describe as v5.12.0-NNN-g...).

✅ Last known-good state — v5.12.0 (1ddafb57)

In 5.12.0 the HTTP layer was simple and honored the user's setting:

  • AnimeApi.Http had only one constructor — Http(String url) — with no
    forceCronet argument
    . No request could force Cronet.
  • initHttpEngine() built Cronet only when Conf.HTTP_CLIENT == 2; otherwise
    Cronet was never touched (if (Conf.HTTP_CLIENT==2){ ... build Cronet ... }).
  • Per-request selection was purely Conf.HTTP_CLIENT-driven: >0 → platform/
    Cronet HttpURLConnection, ==0 → OkHttp. No CDN-based override existed.
  • AnimeView had no effectiveHttpClient logic and no CDN allow-list.

Because nothing forced Cronet, Miruro playback did not depend on GMS at 5.12.0.

❌ Where the Cronet dependency was introduced (all after v5.12.0)

Commit Version base What it introduced
819a6644 v5.12.0-… "triple DataSource switching (OkHttp/Generic/Cronet)" — added the media3 Cronet DataSource path in AnimeView.
3ad23aee v5.12.0-144 "fix(anikoto): vidstream CDN 403 fixes — mewstream cookies, Cronet TLS client selection" — first introduced effectiveHttpClient in AnimeView, overriding Conf.HTTP_CLIENT and forcing Cronet for the Miruro/CF CDN allow-list (mewstream.buzz, nekostream.site, ultracloud.cc, krussdomi.com, etc.).
25a87cd5 (archive branch) "play Miruro ally provider by forcing platform HttpURLConnection" — added the 2-arg Http(url, forceCronet) (and later 3-arg) overload used to force Cronet on the secure pipe (new AnimeApi.Http(pipeUrl, true)).

The net effect of 3ad23aee + 25a87cd5: Miruro requests were force-routed
through Cronet
, which requires Google Play Services. On GMS-less devices
(Fire TV / Firestick) Cronet cannot initialize, so the forced path degraded to a
broken fallback → Miruro unplayable. This is the regression.

Note on version tags: the terminal git describe for these commits reports a
v5.12.0-… base, and the 5.14.0/5.14.2 tags were traced separately in
Finding #9. The important, unambiguous fact is: 5.12.0 is clean; the forced-
Cronet behavior was added in the commits above, after 5.12.0.

✅ How this current work fixes it (branch fix/firestick-black-screen-remove-visibility-toggle)

The fix removes the blanket forced-Cronet routing and restores honoring the
user's setting, with one narrow, availability-guarded exception:

  1. AnimeView video DataSource — the CF-CDN allow-list that forced
    effectiveHttpClient = 2 was replaced. effectiveHttpClient now starts from
    Conf.HTTP_CLIENT; only the Kiwi CDN (owocdn.top, uwucdn.top,
    nekostream.site, kwik.cx) is bumped to Cronet and only if
    AnimeApi.cronetClient != null
    . If Kiwi is seen but Cronet is unavailable
    (GMS-less), it logs and falls back to the user's setting.
  2. Miruro secure pipe — changed new AnimeApi.Http(pipeUrl, true) →
    new AnimeApi.Http(pipeUrl, false), so the pipe fetch honors
    Conf.HTTP_CLIENT instead of forcing Cronet. Browser-like headers + WebView
    cookie forwarding remain to satisfy Cloudflare.

Reverting the diff shows exactly what changed (from git diff HEAD):

-      if (urlLower.contains("mewstream.buzz") || urlLower.contains("nekostream.site")
-          ... force Cronet for the whole CF allow-list ...
+      boolean isKiwiCdn = urlLower.contains("owocdn.top") || urlLower.contains("uwucdn.top")
+          || urlLower.contains("nekostream.site") || urlLower.contains("kwik.cx");
+      if (isKiwiCdn && AnimeApi.cronetClient != null) {
+        effectiveHttpClient = 2; /* Kiwi-only Cronet, guarded */
+      } else if (isKiwiCdn) {
+        /* Cronet unavailable — fall back to Conf.HTTP_CLIENT */
      ...
-          http = new AnimeApi.Http(pipeUrl, true);
+          http = new AnimeApi.Http(pipeUrl, false);

Caveat (see Device Test above): the deployed test build additionally set
FORCE_CRONET_FOR_MIRURO_CDN = false and MIRURO_PIPE_FORCE_CRONET = false.
Under that build, OkHttp plays Miruro but Generic gets stuck. So the final
production fix should also ensure the CF/pipe paths prefer OkHttp when Cronet
is null, rather than allowing the Generic platform client to be used there.

The Cronet error in logcat is benign noise (clarification)

Even when Generic (or OkHttp) is selected, the logcat still shows:

E GooglePlayServicesUtil: GooglePlayServices not available due to error 3
W ATVLOG-API: Cronet primary init failed: All available Cronet providers are disabled.
E ATVLOG-API: Cronet Init FAILED completely. CF-protected CDNs will return 403.
E ATVLOG-API: java.lang.NoClassDefFoundError: ...NativeCronetProvider
   at com.amarullz.androidtv.animetvjmto.AnimeApi.initHttpEngine(AnimeApi.java:658)

This is expected and NOT the cause of the Generic failure. initHttpEngine()
always attempts to build a Cronet engine regardless of the HTTP_CLIENT
setting (see AnimeApi.java ~line 617: "Always initialize Cronet regardless of
HTTP_CLIENT preference"
). On a GMS-less box both Cronet attempts fail and log
this scary line at init time — but Generic requests never touch Cronet, so the
message is misleading. The Cronet Init FAILED line is fired at engine
init, not on a failed playback request.


How the "HTTP Client" Setting Works (Reference)

This section documents how the Settings → HTTP Client option behaves, end to
end, from the JS toggle down to the Java networking layer. Captured here because
it is valuable context that keeps getting re-derived during investigations.

The three options

The setting is stored as Conf.HTTP_CLIENT (an int) and the UI labels come
from pb.cfghttpclient_name in app/src/main/assets/view/modules/18-playback.js:

Value Label Engine TLS fingerprint DoH? Needs GMS?
0 OkHttp (default) OkHttp library OkHttp/BoringSSL ✅ Yes (Cloudflare 1.1.1.1) No
1 Generic HttpClient Android java.net.HttpURLConnection Plain platform/JDK ❌ No No
2 Cronet Chromium net stack (Cronet) Chrome-like (Cronet-managed) Yes (Play Services or embedded lib)

What each option actually does

  • OkHttp (0): The default and most capable client. Uses OkHttp with
    DNS-over-HTTPS. Its TLS handshake is generally accepted by Cloudflare, and DoH
    avoids DNS-based blocks. On GMS-less devices this is the only client that
    plays Miruro.
  • Generic (1): Uses Android's raw HttpURLConnection. A lightweight
    compatibility fallback for when OkHttp misbehaves on a specific device/ROM. It
    has a different, plain TLS fingerprint and no DoH, so Cloudflare-
    protected CDNs (like Miruro) are more likely to reject it. If constructing the
    HttpURLConnection fails, the code silently falls back to OkHttp.
  • Cronet (2): Uses Chrome's network stack, which presents a real Chrome
    TLS fingerprint
    — the best choice for bypassing Cloudflare. However it
    requires Google Play Services
    (play-services-cronet) or the embedded
    Cronet native library (cronet-embedded, currently compileOnly so NOT in
    the APK). On Fire TV / GMS-disabled boxes, Cronet cannot initialize and is
    effectively unavailable.

End-to-end flow (who calls what)

  1. UI toggle (JS): In 18-playback.js, selecting the option updates
    pb.cfg_data.httpclient and calls _JSAPI.setHttpClient(value).
  2. JS→Java bridge: AnimeView.JSViewApi.setHttpClient(int val) runs. It
    skips a no-op re-init (if the value hasn't changed it returns early — this
    guard was added to avoid the Firestick "cache is closed" boot crash), then
    sets Conf.HTTP_CLIENT = val and calls AnimeApi.initHttpEngine(activity).
  3. Engine rebuild: AnimeApi.initHttpEngine() tears down the old engines and
    rebuilds them. Note: it always tries to build Cronet as a fallback
    engine, regardless of the chosen option (this is why the Cronet init error
    appears even under OkHttp/Generic on GMS-less boxes — see the clarification
    above). It then builds the OkHttp bootstrapClient, the DoH client, and sets
    httpClient (with or without DoH based on Conf.USE_DOH).
  4. Per-request selection: When a request is made via AnimeApi.Http:
    • If Conf.HTTP_CLIENT > 0 → it opens a platform HttpURLConnection
      (Generic). Only if HTTP_CLIENT == 2 and cronetClient != null does it
      use Cronet's HttpURLConnection. Any failure falls back to OkHttp.
    • If Conf.HTTP_CLIENT == 0 → it uses OkHttp (req/httpClient) directly.
  5. Video DataSource (ExoPlayer): For playback, AnimeView computes an
    effectiveHttpClient starting from Conf.HTTP_CLIENT. Historically this was
    overridden to force Cronet for Miruro/CF CDNs (the source of this whole
    investigation). The current in-progress direction is to honor the setting and
    only apply a narrow, availability-checked exception.

Practical guidance

  • Default / most users: leave it on OkHttp (0).
  • Fire TV / GMS-disabled: OkHttp (0) — it is the only client that plays
    Miruro there. Generic and Cronet both fail.
  • Generic (1): only as a last-resort compatibility fallback if OkHttp
    misbehaves on a specific device; expect Cloudflare-protected sources to break.
  • Cronet (2): only meaningful when Google Play Services (or an embedded
    Cronet lib) is present; gives the strongest Cloudflare bypass via Chrome TLS.

Comparison

Condition Cronet available? Miruro TLS fingerprint Result
GMS enabled Yes (Play Services Cronet) Chrome-like ✅ Plays
GMS disabled No (embedded is compileOnly, not in APK) Platform/OkHttp ❌ 400/403, no playback

Failure Scenarios

  • Fire TV / Firestick (PRIMARY IMPACT): Fire OS is a forked Android that
    ships without Google Play Services by default. Therefore stock Fire TV
    and Firestick devices have no play-services-cronet runtime, cronetClient
    is always null, and Miruro is unplayable out of the box for essentially the
    entire Fire TV user base (unless they've sideloaded microG/GMS, which most
    have not). The GMS-disabled emulator test reproduced exactly the stock Fire TV
    condition. This elevates the issue from an edge case to a platform-wide
    outage on a primary target device for a TV app.

  • GMS-disabled / degraded devices: Any device where the user disables Play
    Services, or where GMS is stale/broken, loses Miruro entirely.

  • Non-GMS devices (AOSP TV boxes, de-Googled ROMs): Same failure — Miruro
    never works because there is no embedded Cronet fallback shipped.

  • Silent-ish failure: The error is only visible in logcat
    (Cronet Init FAILED completely / forceCronet FALLBACK); the user just
    sees a stream that won't play.


Immediate (diagnosis confirmation)

  • Reproduce with logcat filtered on the AnimeApi tag and confirm the presence
    of Cronet Init FAILED completely. CF-protected CDNs will return 403. while
    Play Services is disabled.

Short-term (fix the fallback)

  • Change the embedded Cronet dependency from compileOnly to implementation
    so the self-contained native library ships in the APK:
    implementation 'org.chromium.net:cronet-embedded:119.6045.31'
    
    This makes Attempt 2 actually succeed when GMS is unavailable, removing
    the hard dependency on Google Play Services for Miruro playback.
    • Trade-off: APK size increases significantly (embedded Cronet bundles
      a full Chromium net stack native lib, tens of MB). Consider ABI splits /
      per-architecture APKs to mitigate.
    • Confirm the -maximumremovedandroidloglevel ProGuard rule issue noted in
      the gradle comment is handled if switching to implementation.

Long-term (architecture)

  • Consider only shipping embedded Cronet for non-GMS ABIs/flavors, or a build
    flavor split (GMS vs. non-GMS) to balance APK size against reliability.
  • Add a user-visible diagnostic when cronetClient == null and a Miruro
    playback is attempted, so the failure is not silent.

Files Involved

File Role
app/src/main/java/com/amarullz/androidtv/animetvjmto/AnimeApi.java initHttpEngine() Cronet init + Http ctor fallback logic
app/build.gradle Cronet dependency declarations (play-services-cronet = implementation, cronet-embedded = compileOnly)
electron/src/data/datasources/miruro-pipe.js Miruro secure-pipe resolution (reference for the same Cloudflare/TLS requirement on desktop)

  • Current HEAD: 620a28775b48cac5cb808caf770be4d1a8b97a78

Assumptions

  • The emulator's Play Services provided the working Cronet engine while enabled.
  • No res/xml/network_security_config.xml or custom TLS/TrustManager overrides
    exist (verified: none found in the animetvjmto package), so TLS behavior is
    purely a function of which HTTP engine (Cronet vs OkHttp/platform) is used.
# Miruro Playback Fails When Google Play Services Is Disabled (Cronet/GMS Dependency) ## Header | Field | Value | |-------|-------| | **Date** | 2026-07-03 | | **Reporter** | User (emulator testing) | | **Platform** | Android emulator (Google Play Services toggled off/on) | | **Component** | HTTP engine (`AnimeApi.initHttpEngine`), Miruro source (SD8) | | **HTTP client** | OkHttpClient (default TLS) + Cronet (fallback) | | **Status** | **IN PROGRESS — findings revised** (2026-07-05 device test corrected the earlier "RESOLVED" conclusion; see "Device Test — 2026-07-05" below) | | **Severity** | **Critical** — Miruro is unplayable on **all stock Fire TV / Firestick devices** (Fire OS has no Google Play Services) plus any non-GMS / GMS-disabled Android device | --- ## Summary Miruro videos stop playing after Google Play Services is **disabled**, and start playing again once it is **re-enabled**. The app relies on **Cronet (Chromium's network stack)** to produce a Chrome-like TLS fingerprint for Miruro's Cloudflare-protected CDNs. Cronet is loaded from **Google Play Services** (`play-services-cronet`, bundled via `implementation`), while the offline fallback (`cronet-embedded`) is declared **`compileOnly`** and therefore is **NOT packaged in the APK**. When Play Services is disabled, no Cronet engine can initialize, so Miruro requests fall back to OkHttp / the platform HTTP stack, whose TLS fingerprint Cloudflare rejects (HTTP 400/403) — breaking playback. --- ## Key Findings ### 1. Miruro requires Cronet's TLS fingerprint Miruro's secure pipe and its CDNs sit behind Cloudflare, which rejects non-Chrome TLS fingerprints. The code documents this explicitly: `AnimeApi.java` (Http constructor, ~line 722): ```java /* forceCronet routes the request through Cronet's HttpURLConnection * regardless of Conf.HTTP_CLIENT. This mirrors the Electron fix that * routes Cloudflare TLS-strict endpoints (e.g. the Miruro secure pipe) * through Chromium's network stack. OkHttp's TLS fingerprint is rejected * by Cloudflare with HTTP 400, so when forceCronet is true we never fall * back to OkHttp. */ ``` ### 2. Cronet is loaded from Google Play Services (the dependency at risk) `app/build.gradle`: ```gradle implementation 'com.google.android.gms:play-services-cronet:18.1.0' // ... compileOnly 'org.chromium.net:cronet-embedded:119.6045.31' ``` - `play-services-cronet` is **`implementation`** → the Cronet *provider* is bundled, but the actual Cronet engine is delivered **at runtime by Google Play Services**. If Play Services is disabled/absent, this provider has nothing to load. - `cronet-embedded` (the self-contained native fallback that would NOT need GMS) is **`compileOnly`** → its native `.so` is intentionally excluded from the APK to keep size down. ### 3. Both Cronet init attempts fail without GMS `AnimeApi.initHttpEngine()`: **Attempt 1** — auto-discovery relies on the Play Services provider: ```java CronetEngine.Builder myBuilder = new CronetEngine.Builder(c) ... ; cronetClient = myBuilder.build(); // fails when GMS disabled ``` **Attempt 2** — explicit embedded native provider: ```java org.chromium.net.impl.NativeCronetProvider nativeProvider = new org.chromium.net.impl.NativeCronetProvider(c); // NoClassDefFoundError ... } catch (Throwable embeddedException) { // Catches NoClassDefFoundError when cronet-embedded is compileOnly Log.e(_TAG,"Cronet Init FAILED completely. CF-protected CDNs will return 403.",embeddedException); cronetClient = null; } ``` Because `cronet-embedded` is `compileOnly`, the native classes are missing at runtime → `NoClassDefFoundError` → `cronetClient == null`. ### 4. Fallback path reproduces the Cloudflare rejection With `cronetClient == null`, a `forceCronet` request degrades to the platform `HttpURLConnection`: ```java if (forceCronet){ Log.e(_TAG,"forceCronet FALLBACK to platform HttpURLConnection — " +"Cronet unavailable; Cloudflare-protected requests will likely 400"); } URL netConn = new URL(url); http = (HttpURLConnection) netConn.openConnection(); ``` The platform/OkHttp TLS fingerprint is rejected by Cloudflare (HTTP 400/403), so the Miruro stream/pipe never resolves → **no playback**. ### 5. The `HTTP_CLIENT` settings toggle is bypassed for Miruro CDNs User tested switching the app's HTTP client setting to "generic HTTP client" and saw **identical broken behavior** while GMS was disabled. This is expected and further confirms the root cause: for Miruro's Cloudflare-protected CDNs, the DataSource selection **ignores the user's `HTTP_CLIENT` setting** and computes its own `effectiveHttpClient`. `AnimeView.java` (video DataSource factory, ~line 1088): ```java int effectiveHttpClient = Conf.HTTP_CLIENT; String urlLower = videoStatCurrentUrl.toLowerCase(); if (urlLower.contains("mewstream.buzz") || urlLower.contains("nekostream.site") || urlLower.contains("ultracloud.cc") || urlLower.contains("krussdomi.com") || urlLower.contains("lostproject.club") || urlLower.contains("anizara.store") || urlLower.contains("cloudbuzz.lol") || urlLower.contains("trycloud.pro")) { if (AnimeApi.cronetClient != null) { effectiveHttpClient = 2; /* Cronet — Chrome TLS fingerprint passes CF */ } else if (AnimeApi.httpClient != null) { effectiveHttpClient = 0; /* OkHttp fallback if Cronet unavailable */ } } ``` Consequence table (GMS disabled → `cronetClient == null`): | User setting | What actually runs for Miruro CDN | Result | |--------------|-----------------------------------|--------| | 0 (OkHttp) | OkHttp (forced fallback) | ❌ CF 403 | | 1 (Generic) | OkHttp (forced fallback, setting ignored) | ❌ CF 403 | | 2 (Cronet) | OkHttp (Cronet null, forced fallback) | ❌ CF 403 | So no `HTTP_CLIENT` choice can fix Miruro while GMS is disabled — only a working Cronet engine can. This is precisely why the "generic HTTP client" test showed the same behavior. It also means the *only* setting that can differ is when GMS is **enabled** and Cronet exists: then Miruro is forced onto Cronet regardless of the toggle. ### 6. Why re-enabling GMS "fixes" it Re-enabling Play Services restores the Cronet provider → Attempt 1 succeeds → `cronetClient != null` → Miruro requests use the Chrome-like TLS fingerprint → Cloudflare accepts them → playback works again. This exactly matches the reported off → broken, on → working behavior. ### 7. Confirming logcat evidence (GMS disabled) Captured while GMS was disabled and the HTTP client was switched to "generic", which triggered a `setHttpClient` → `initHttpEngine` re-init: ``` E GooglePlayServicesUtil: GooglePlayServices not available due to error 3 I PlayServicesCronet: Google-Play-Services-Cronet-Provider is not installed yet. W ATVLOG-API: Cronet primary init failed: All available Cronet providers are disabled. E ATVLOG-API: Cronet Init FAILED completely. CF-protected CDNs will return 403. E ATVLOG-API: java.lang.NoClassDefFoundError: Failed resolution of: Lorg/chromium/net/impl/NativeCronetProvider; Caused by: java.lang.ClassNotFoundException: Didn't find class "org.chromium.net.impl.NativeCronetProvider" on path: DexPathList[...] at com.amarullz.androidtv.animetvjmto.AnimeApi.initHttpEngine(AnimeApi.java:658) ``` This is the exact two-attempt failure chain the analysis predicted: 1. **Attempt 1 (Play Services Cronet) fails** — `GooglePlayServices not available due to error 3` (error code 3 = `SERVICE_DISABLED`) → "All available Cronet providers are disabled." 2. **Attempt 2 (embedded native) fails** — `NoClassDefFoundError` / `ClassNotFoundException` for `org.chromium.net.impl.NativeCronetProvider`, because the `compileOnly` `cronet-embedded` native lib is **not present in the APK's DexPathList**. This is the direct proof of Finding #2/#3. 3. **Result** — `cronetClient == null` → "CF-protected CDNs will return 403." **Secondary (non-root-cause) noise:** the `java.net.SocketException: Socket closed` and `java.io.IOException: Canceled` entries occur because switching the HTTP client setting calls `AnimeApi.initHttpEngine()`, which runs `dispatcher().cancelAll()` and shuts down the OkHttp executor — cancelling the in-flight home-feed requests (aniwave/miruro trending) that were mid-TLS handshake. These are a benign side effect of the live re-init, not the Miruro playback failure. ### 8. Visible symptom (user-facing) With GMS disabled, playing a Miruro title shows an **indefinite "LOADING DATA" spinner on a black screen with no playback controls** (see reporter screenshot, Pixel_4_XL emulator). The stream URL never resolves (Cloudflare rejects the non-Cronet requests), so ExoPlayer never receives a playable manifest → the player UI (menu / playback buttons) is never built and the spinner never clears. With GMS enabled, the same title loads the manifest, shows the normal playback menu buttons, and plays — matching the reporter's description. This confirms the failure is at **stream resolution**, before ExoPlayer can render anything, which is exactly what a null `cronetClient` + Cloudflare 400/403 produces. ### 9. Version regression timeline (git-traced) Traced via `app/build.gradle` history across the 5.x tags: | Version | `play-services-cronet` (GMS) | Cronet DataSource routing | `compileOnly cronet-embedded` fallback | |---------|------------------------------|---------------------------|-----------------------------------------| | 5.13.0 | ✅ present | ❌ not yet | ❌ absent | | 5.14.0 | ✅ present | ✅ added (`819a6644`) | ❌ absent | | **5.14.1** | ✅ present | ✅ present | ❌ **absent** | | 5.14.2 | ✅ present | ✅ present | ✅ added (`compileOnly`) | | 5.14.5 (HEAD) | ✅ present | ✅ present | ✅ present (`compileOnly`) | **Conclusion on "was this introduced after 5.14.1?":** No. - The behavior that makes Miruro playback **depend on Cronet** (and thus GMS) was introduced in **v5.14.0** via commit `819a6644` ("feat: implement triple DataSource switching (OkHttp/Generic/Cronet)"), which added the media3 Cronet DataSource and forced Miruro CDNs onto Cronet. This is *at/before* 5.14.1 — so **5.14.1 is already affected**. - The `compileOnly cronet-embedded` line was added later, in **v5.14.2**. Because `compileOnly` does not package the native library, it never provided a real offline fallback; it only causes the second init attempt to fail with `NoClassDefFoundError`. So 5.14.2+ did not fix (nor introduce) the core issue — it merely added a non-functional fallback attempt. - `play-services-cronet` itself predates all of this (present since ≥ 5.13.0), but was harmless until 5.14.0 wired Miruro playback through Cronet. --- ## Test Result — Cronet-forcing Disabled (2026-07-04) To validate the root cause, both Cronet-forcing layers for Miruro were **temporarily disabled** behind TEST-ONLY toggles (on branch `experiment/miruro-cronet-play-services-fix`), so the user's `HTTP_CLIENT` setting is honored instead of being overridden to Cronet: 1. `AnimeView.java` video DataSource — `FORCE_CRONET_FOR_MIRURO_CDN = false` (skips the CF-CDN override in the `videoDataSourceFactory`). 2. `AnimeView.java` Miruro secure pipe — `MIRURO_PIPE_FORCE_CRONET = false` (the pipe fetch that resolves the stream URL was independently forcing Cronet; it now honors `Conf.HTTP_CLIENT` too). **Result:** With GMS **disabled** and Cronet-forcing off, **Miruro plays** using the non-Cronet HTTP client. The build compiled cleanly and playback worked on the emulator without Google Play Services. ### What this changes about the root cause The original Findings #4–#6 predicted that *any* non-Cronet client would be rejected by Cloudflare (400/403), implying only a working Cronet engine could play Miruro. The test **disproves the absolute form of that claim**: at least for the CDN/pipe paths exercised in this test, OkHttp / the platform client **can** reach Miruro without Cronet. Revised understanding: the GMS-disabled failure was caused primarily by the **forced-Cronet routing degrading to a broken fallback**, not by an inherent Cloudflare requirement for a Chrome TLS fingerprint on every Miruro request. When the code forces Cronet and Cronet is null, the fallback path (and/or its header/config assumptions) fails — whereas letting OkHttp handle the request normally succeeds. ### Implication for the fix This reopens a **cheaper alternative** to shipping `cronet-embedded` (which the user has confirmed is undesirable due to the ~20–30 MB APK size increase): instead of bundling Cronet, **stop force-routing Miruro CDNs through Cronet when `cronetClient == null`** and let the existing OkHttp/platform client handle them. This avoids the GMS dependency **and** avoids the APK bloat. **Still pending:** the user is testing all Miruro providers (ally, bee, pewe, bonk, hop, kiwi, etc.) with Cronet-forcing disabled to confirm none of them actually require the Chrome TLS fingerprint. If some providers still need Cronet, a targeted per-provider decision (rather than a blanket force) would be the correct fix. --- ## Final Resolution (2026-07-05) — SUPERSEDED (see "Device Test — 2026-07-05" below) > ⚠️ **This "RESOLVED" section was written before the 2026-07-05 on-device test > on a real GMS-disabled Android TV box. That test contradicts the assumptions > here: with the setting honored, "Generic HttpClient" and "OkHttp" behaved > DIFFERENTLY (Generic = stuck loading, OkHttp = plays). The conclusions in this > section are retained for history but are NOT the final resolution. Read > "Device Test — 2026-07-05" for the corrected findings.** **Fix implemented (Option A): honor the user's `HTTP_CLIENT` setting; force Cronet only for the Kiwi CDN when a Cronet engine exists.** Following the 2026-07-04 test that proved Miruro plays over OkHttp/platform without Cronet, the two blanket Cronet-forcing layers were removed and replaced with the user's setting. Testing across Miruro providers confirmed only the **Kiwi CDN** (`owocdn.top`, `uwucdn.top`, `nekostream.site`, `kwik.cx`) still needs Cronet's Chrome TLS fingerprint. This narrow exception is applied **only when `AnimeApi.cronetClient != null`**; otherwise it falls back to the user's `HTTP_CLIENT` setting (no broken forced-null path). ### Changes (in `AnimeView.java`) 1. **Video DataSource factory (~line 1088):** removed the blanket CF-CDN Cronet override. `effectiveHttpClient` now starts from `Conf.HTTP_CLIENT` and is only bumped to Cronet (`2`) for Kiwi URLs **if** a Cronet engine is available; if Kiwi is detected but Cronet is null, it logs and falls back to the setting. 2. **Miruro secure pipe (~line 2837):** the pipe fetch now uses `new AnimeApi.Http(pipeUrl, false)` (forceCronet=false), honoring the setting. Browser-like headers + WebView cookie forwarding remain to satisfy Cloudflare. ### Outcome - Miruro plays on **GMS-disabled / Fire TV** devices using OkHttp/platform. - The Kiwi CDN still gets Cronet when GMS is present, preserving its playback. - **No APK bloat** — `cronet-embedded` remains `compileOnly`; we did not bundle the embedded Chromium net stack. ### Source isolation note Per the source-isolation rule, these changes affect only the Miruro (SD7/SD8) paths and the Kiwi exception within them; no other source's CDN resolution was touched. --- ## Device Test — 2026-07-05 (real GMS-disabled Android TV box) — CORRECTED FINDINGS **Setup:** A physical Android TV box at `192.168.100.106` with **Google Play Services disabled**, deliberately configured to emulate a stock Fire TV / Firestick (no GMS). A debug build was deployed via `./deploy.sh`. The deployed `AnimeView.java` was the **TEST-TOGGLE variant** — both Cronet-forces disabled (`FORCE_CRONET_FOR_MIRURO_CDN = false`, `MIRURO_PIPE_FORCE_CRONET = false`) — so the user's `HTTP_CLIENT` setting is fully honored for every Miruro request (no Cronet forcing anywhere). **Results (Miruro playback, GMS disabled):** | `HTTP_CLIENT` setting | Engine actually used | Miruro result | |-----------------------|----------------------|---------------| | 0 — OkHttp | OkHttp + DoH | ✅ **Plays** | | 1 — Generic | Platform `HttpURLConnection` | ❌ **Stuck on "LOADING DATA"** | | 2 — Cronet | Cronet → unavailable (no GMS) | ❌ Fails (engine never inits) | **This corrects the earlier conclusions.** The 2026-07-04 test and the "RESOLVED" section assumed OkHttp and Generic behave the same. They do **not**: - **OkHttp works** on the GMS-disabled box. OkHttp's TLS fingerprint + DoH is accepted by Cloudflare on the Miruro pipe/CDNs. - **Generic does NOT work.** The plain platform `HttpURLConnection` has a different (plain-JDK) TLS fingerprint and no DoH, and Cloudflare does not accept it for the Miruro pipe → the stream URL never resolves → indefinite "LOADING DATA". - **Cronet cannot even initialize** without GMS (see Findings #2/#3). ### Revised root-cause conclusion The original IA premise — *"Miruro requires Cronet"* — is **wrong for GMS-less devices**. On a real Fire-TV-like device, **OkHttp is the ONLY client that both (a) initializes without GMS and (b) passes Cloudflare.** Forcing Cronet on such devices is the worst option (it is unavailable), and Generic is the weakest (no DoH, non-Chrome TLS). ### Corrected fix direction (proposed, not yet implemented) For the Miruro secure pipe and CF-protected CDNs, the correct behavior on GMS-less devices is to **force OkHttp** (not Cronet, not Generic) — i.e. prefer `AnimeApi.httpClient` when `cronetClient == null`, rather than degrading to the platform `HttpURLConnection` or honoring a Generic setting that Cloudflare rejects. This keeps Miruro playable on Fire TV without APK bloat. --- ## Regression History — Where It Broke and Where It Worked (git-traced) This section preserves the exact commit history so the regression is not lost. All commits are relative to tag **`v5.12.0`** (commit `1ddafb57`, dated 2026-05-26), because every relevant commit sits *after* 5.12.0 (they all `git describe` as `v5.12.0-NNN-g...`). ### ✅ Last known-good state — v5.12.0 (`1ddafb57`) In 5.12.0 the HTTP layer was **simple and honored the user's setting**: - `AnimeApi.Http` had **only one constructor** — `Http(String url)` — with **no `forceCronet` argument**. No request could force Cronet. - `initHttpEngine()` built Cronet **only when `Conf.HTTP_CLIENT == 2`**; otherwise Cronet was never touched (`if (Conf.HTTP_CLIENT==2){ ... build Cronet ... }`). - Per-request selection was purely `Conf.HTTP_CLIENT`-driven: `>0` → platform/ Cronet `HttpURLConnection`, `==0` → OkHttp. No CDN-based override existed. - `AnimeView` had **no `effectiveHttpClient`** logic and **no CDN allow-list**. Because nothing forced Cronet, Miruro playback did not depend on GMS at 5.12.0. ### ❌ Where the Cronet dependency was introduced (all after v5.12.0) | Commit | Version base | What it introduced | |--------|--------------|--------------------| | `819a6644` | `v5.12.0-…` | "triple DataSource switching (OkHttp/Generic/Cronet)" — added the media3 Cronet DataSource path in `AnimeView`. | | `3ad23aee` | `v5.12.0-144` | "fix(anikoto): vidstream CDN 403 fixes — mewstream cookies, **Cronet TLS client selection**" — **first introduced `effectiveHttpClient`** in `AnimeView`, overriding `Conf.HTTP_CLIENT` and forcing Cronet for the Miruro/CF CDN allow-list (`mewstream.buzz`, `nekostream.site`, `ultracloud.cc`, `krussdomi.com`, etc.). | | `25a87cd5` | (archive branch) | "play Miruro ally provider by forcing platform HttpURLConnection" — added the **2-arg `Http(url, forceCronet)`** (and later 3-arg) overload used to force Cronet on the secure pipe (`new AnimeApi.Http(pipeUrl, true)`). | The net effect of `3ad23aee` + `25a87cd5`: Miruro requests were **force-routed through Cronet**, which requires Google Play Services. On GMS-less devices (Fire TV / Firestick) Cronet cannot initialize, so the forced path degraded to a broken fallback → Miruro unplayable. This is the regression. > Note on version tags: the terminal `git describe` for these commits reports a > `v5.12.0-…` base, and the `5.14.0`/`5.14.2` tags were traced separately in > Finding #9. The important, unambiguous fact is: **5.12.0 is clean; the forced- > Cronet behavior was added in the commits above, after 5.12.0.** ### ✅ How this current work fixes it (branch `fix/firestick-black-screen-remove-visibility-toggle`) The fix **removes the blanket forced-Cronet routing** and restores honoring the user's setting, with one narrow, availability-guarded exception: 1. **`AnimeView` video DataSource** — the CF-CDN allow-list that forced `effectiveHttpClient = 2` was replaced. `effectiveHttpClient` now starts from `Conf.HTTP_CLIENT`; only the **Kiwi CDN** (`owocdn.top`, `uwucdn.top`, `nekostream.site`, `kwik.cx`) is bumped to Cronet **and only if `AnimeApi.cronetClient != null`**. If Kiwi is seen but Cronet is unavailable (GMS-less), it logs and falls back to the user's setting. 2. **Miruro secure pipe** — changed `new AnimeApi.Http(pipeUrl, true)` → `new AnimeApi.Http(pipeUrl, false)`, so the pipe fetch honors `Conf.HTTP_CLIENT` instead of forcing Cronet. Browser-like headers + WebView cookie forwarding remain to satisfy Cloudflare. **Reverting the diff shows exactly what changed** (from `git diff HEAD`): ```diff - if (urlLower.contains("mewstream.buzz") || urlLower.contains("nekostream.site") - ... force Cronet for the whole CF allow-list ... + boolean isKiwiCdn = urlLower.contains("owocdn.top") || urlLower.contains("uwucdn.top") + || urlLower.contains("nekostream.site") || urlLower.contains("kwik.cx"); + if (isKiwiCdn && AnimeApi.cronetClient != null) { + effectiveHttpClient = 2; /* Kiwi-only Cronet, guarded */ + } else if (isKiwiCdn) { + /* Cronet unavailable — fall back to Conf.HTTP_CLIENT */ ... - http = new AnimeApi.Http(pipeUrl, true); + http = new AnimeApi.Http(pipeUrl, false); ``` **Caveat (see Device Test above):** the deployed test build additionally set `FORCE_CRONET_FOR_MIRURO_CDN = false` and `MIRURO_PIPE_FORCE_CRONET = false`. Under that build, **OkHttp plays Miruro but Generic gets stuck**. So the final production fix should also ensure the CF/pipe paths **prefer OkHttp** when Cronet is null, rather than allowing the Generic platform client to be used there. ### The Cronet error in logcat is benign noise (clarification) Even when Generic (or OkHttp) is selected, the logcat still shows: ``` E GooglePlayServicesUtil: GooglePlayServices not available due to error 3 W ATVLOG-API: Cronet primary init failed: All available Cronet providers are disabled. E ATVLOG-API: Cronet Init FAILED completely. CF-protected CDNs will return 403. E ATVLOG-API: java.lang.NoClassDefFoundError: ...NativeCronetProvider at com.amarullz.androidtv.animetvjmto.AnimeApi.initHttpEngine(AnimeApi.java:658) ``` This is **expected and NOT the cause of the Generic failure.** `initHttpEngine()` **always** attempts to build a Cronet engine regardless of the `HTTP_CLIENT` setting (see `AnimeApi.java` ~line 617: *"Always initialize Cronet regardless of HTTP_CLIENT preference"*). On a GMS-less box both Cronet attempts fail and log this scary line at init time — but Generic requests never touch Cronet, so the message is misleading. The `Cronet Init FAILED` line is fired at engine init, not on a failed playback request. --- ## How the "HTTP Client" Setting Works (Reference) This section documents how the **Settings → HTTP Client** option behaves, end to end, from the JS toggle down to the Java networking layer. Captured here because it is valuable context that keeps getting re-derived during investigations. ### The three options The setting is stored as `Conf.HTTP_CLIENT` (an `int`) and the UI labels come from `pb.cfghttpclient_name` in `app/src/main/assets/view/modules/18-playback.js`: | Value | Label | Engine | TLS fingerprint | DoH? | Needs GMS? | |-------|-------|--------|-----------------|------|------------| | 0 | **OkHttp** (default) | OkHttp library | OkHttp/BoringSSL | ✅ Yes (Cloudflare `1.1.1.1`) | No | | 1 | **Generic HttpClient** | Android `java.net.HttpURLConnection` | Plain platform/JDK | ❌ No | No | | 2 | **Cronet** | Chromium net stack (Cronet) | **Chrome-like** | (Cronet-managed) | **Yes** (Play Services or embedded lib) | ### What each option actually does - **OkHttp (0):** The default and most capable client. Uses OkHttp with DNS-over-HTTPS. Its TLS handshake is generally accepted by Cloudflare, and DoH avoids DNS-based blocks. **On GMS-less devices this is the only client that plays Miruro.** - **Generic (1):** Uses Android's raw `HttpURLConnection`. A lightweight compatibility fallback for when OkHttp misbehaves on a specific device/ROM. It has a **different, plain TLS fingerprint** and **no DoH**, so Cloudflare- protected CDNs (like Miruro) are more likely to reject it. If constructing the `HttpURLConnection` fails, the code silently falls back to OkHttp. - **Cronet (2):** Uses Chrome's network stack, which presents a **real Chrome TLS fingerprint** — the best choice for bypassing Cloudflare. **However it requires Google Play Services** (`play-services-cronet`) **or** the embedded Cronet native library (`cronet-embedded`, currently `compileOnly` so NOT in the APK). On Fire TV / GMS-disabled boxes, Cronet **cannot initialize** and is effectively unavailable. ### End-to-end flow (who calls what) 1. **UI toggle (JS):** In `18-playback.js`, selecting the option updates `pb.cfg_data.httpclient` and calls `_JSAPI.setHttpClient(value)`. 2. **JS→Java bridge:** `AnimeView.JSViewApi.setHttpClient(int val)` runs. It **skips a no-op re-init** (if the value hasn't changed it returns early — this guard was added to avoid the Firestick "cache is closed" boot crash), then sets `Conf.HTTP_CLIENT = val` and calls `AnimeApi.initHttpEngine(activity)`. 3. **Engine rebuild:** `AnimeApi.initHttpEngine()` tears down the old engines and rebuilds them. **Note:** it *always* tries to build Cronet as a fallback engine, regardless of the chosen option (this is why the Cronet init error appears even under OkHttp/Generic on GMS-less boxes — see the clarification above). It then builds the OkHttp `bootstrapClient`, the DoH client, and sets `httpClient` (with or without DoH based on `Conf.USE_DOH`). 4. **Per-request selection:** When a request is made via `AnimeApi.Http`: - If `Conf.HTTP_CLIENT > 0` → it opens a platform `HttpURLConnection` (Generic). Only if `HTTP_CLIENT == 2` **and** `cronetClient != null` does it use Cronet's `HttpURLConnection`. Any failure falls back to OkHttp. - If `Conf.HTTP_CLIENT == 0` → it uses OkHttp (`req`/`httpClient`) directly. 5. **Video DataSource (ExoPlayer):** For playback, `AnimeView` computes an `effectiveHttpClient` starting from `Conf.HTTP_CLIENT`. Historically this was **overridden** to force Cronet for Miruro/CF CDNs (the source of this whole investigation). The current in-progress direction is to honor the setting and only apply a narrow, availability-checked exception. ### Practical guidance - **Default / most users:** leave it on **OkHttp (0)**. - **Fire TV / GMS-disabled:** **OkHttp (0)** — it is the only client that plays Miruro there. Generic and Cronet both fail. - **Generic (1):** only as a last-resort compatibility fallback if OkHttp misbehaves on a specific device; expect Cloudflare-protected sources to break. - **Cronet (2):** only meaningful when Google Play Services (or an embedded Cronet lib) is present; gives the strongest Cloudflare bypass via Chrome TLS. --- ## Comparison | Condition | Cronet available? | Miruro TLS fingerprint | Result | |-----------|-------------------|------------------------|--------| | GMS enabled | Yes (Play Services Cronet) | Chrome-like | ✅ Plays | | GMS disabled | No (embedded is `compileOnly`, not in APK) | Platform/OkHttp | ❌ 400/403, no playback | --- ## Failure Scenarios - **Fire TV / Firestick (PRIMARY IMPACT)**: Fire OS is a forked Android that ships **without Google Play Services** by default. Therefore *stock* Fire TV and Firestick devices have no `play-services-cronet` runtime, `cronetClient` is always null, and Miruro is unplayable out of the box for essentially the entire Fire TV user base (unless they've sideloaded microG/GMS, which most have not). The GMS-disabled emulator test reproduced exactly the stock Fire TV condition. This elevates the issue from an edge case to a **platform-wide outage on a primary target device for a TV app.** - **GMS-disabled / degraded devices**: Any device where the user disables Play Services, or where GMS is stale/broken, loses Miruro entirely. - **Non-GMS devices** (AOSP TV boxes, de-Googled ROMs): Same failure — Miruro never works because there is no embedded Cronet fallback shipped. - **Silent-ish failure**: The error is only visible in logcat (`Cronet Init FAILED completely` / `forceCronet FALLBACK`); the user just sees a stream that won't play. --- ## Recommended Actions ### Immediate (diagnosis confirmation) - Reproduce with logcat filtered on the `AnimeApi` tag and confirm the presence of `Cronet Init FAILED completely. CF-protected CDNs will return 403.` while Play Services is disabled. ### Short-term (fix the fallback) - Change the embedded Cronet dependency from `compileOnly` to `implementation` so the self-contained native library ships in the APK: ```gradle implementation 'org.chromium.net:cronet-embedded:119.6045.31' ``` This makes **Attempt 2** actually succeed when GMS is unavailable, removing the hard dependency on Google Play Services for Miruro playback. - **Trade-off:** APK size increases significantly (embedded Cronet bundles a full Chromium net stack native lib, tens of MB). Consider ABI splits / per-architecture APKs to mitigate. - Confirm the `-maximumremovedandroidloglevel` ProGuard rule issue noted in the gradle comment is handled if switching to `implementation`. ### Long-term (architecture) - Consider only shipping embedded Cronet for non-GMS ABIs/flavors, or a build flavor split (GMS vs. non-GMS) to balance APK size against reliability. - Add a user-visible diagnostic when `cronetClient == null` and a Miruro playback is attempted, so the failure is not silent. --- ## Files Involved | File | Role | |------|------| | `app/src/main/java/com/amarullz/androidtv/animetvjmto/AnimeApi.java` | `initHttpEngine()` Cronet init + `Http` ctor fallback logic | | `app/build.gradle` | Cronet dependency declarations (`play-services-cronet` = implementation, `cronet-embedded` = compileOnly) | | `electron/src/data/datasources/miruro-pipe.js` | Miruro secure-pipe resolution (reference for the same Cloudflare/TLS requirement on desktop) | --- ## Related Commits - Current HEAD: `620a28775b48cac5cb808caf770be4d1a8b97a78` --- ## Assumptions - The emulator's Play Services provided the working Cronet engine while enabled. - No `res/xml/network_security_config.xml` or custom TLS/TrustManager overrides exist (verified: none found in the `animetvjmto` package), so TLS behavior is purely a function of which HTTP engine (Cronet vs OkHttp/platform) is used.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Anime-TV-Fork/AnimeTV#2
No description provided.