Skip to content

feat: add opt-in Widevine L3 fallback for DRM playback failures - #120

Merged
Balamurugan-100 merged 11 commits into
mainfrom
feat-L3-fallback-DRM-issue
Aug 18, 2026
Merged

feat: add opt-in Widevine L3 fallback for DRM playback failures#120
Balamurugan-100 merged 11 commits into
mainfrom
feat-L3-fallback-DRM-issue

Conversation

@Balamurugan-100

Copy link
Copy Markdown
Member

This adds automatic fallback to L3 (software) decryption when DRM provisioning fails on L1 (hardware-secured) devices, providing graceful degradation instead of complete playback failure.

Key changes:

  • TPStreamsPlayer.kt: Added drmSecurityLevel (cached via DecoderInfoProvider), forceL3ForSession flag (session-wide), strict isDrmLicenseProvisioningError() checker (excludes LICENSE_EXPIRED - has separate renewal path), attemptDrmFallback() using existing MediaCodecSelector mechanism (same as debug-l3-force-app), preparePlayer() L3 reconfiguration when forceL3ForSession=true, and onPlayerError() conditional fallback.
  • SentryLogger.kt: Added drmSecurityLevel parameter (replaces hardcoded "L1"), widevine_security_level tag uses actual cached level, rootCause tag is DRM_L3_FALLBACK or DRM_LICENSE_ACQUISITION_FAILED.

@Balamurugan-100
Balamurugan-100 force-pushed the feat-L3-fallback-DRM-issue branch from 410921b to 98fc40b Compare August 15, 2026 16:13
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @Balamurugan-100's task in 3m 12s —— View job


Summary

Reviewed the latest changes to the DRM L3-fallback feature (round 3) against the two previous review rounds. All previously-flagged compile-breaking issues remain fixed, and several correctness issues from round 2 are resolved. However, a new change in this round makes the fallback mechanism functionally inert from the host app's perspective, which is a blocking regression against the PR's stated purpose.

Confirmed fixed since round 2:

Findings

1. onPlayerError now unconditionally reports every DRM provisioning error to the host app — the fallback no longer changes observable behavior at all (blocking)
TPStreamsPlayer.kt:400-427. In round 2, the bug was that onPlayerError ignored attemptDrmFallback()'s return value and always returned, silently swallowing errors. The fix in this round removes the early return entirely — but now both branches (fallbackSucceeded true or false) fall through to SentryLogger.logPlaybackException(...) and _listener?.onError(errorType, errorMessage) unconditionally. The inline comment even states this explicitly: "We do NOT return early... so we always report the error to the host app."

The practical effect: a host app integrating this SDK will see onError() fired for every DRM provisioning failure, exactly as it did before this PR existed. The only side effects of attemptDrmFallback() are an async exoPlayer.prepare() retry racing in the background and one extra Sentry tag. Since most host apps react to onError by tearing down the player or showing a terminal error UI (that's the whole reason this PR exists — to avoid that), the queued re-prepare is very likely to be racing against — or made moot by — the host's own error-handling reaction. This directly contradicts the PR's stated goal of "graceful degradation instead of complete playback failure": there is currently no code path where a provisioning error is fully resolved without the app being told playback failed.
Fix this →

2. attemptDrmFallback() still doesn't force L3 — carried over from round 2, still unaddressed (blocking)
TPStreamsPlayer.kt:479-522. This was flagged in the previous round: the function sets forceL3ForSession = true and re-calls exoPlayer.prepare() on the same currentMediaItem/DrmConfiguration — nothing about the Widevine security level, ExoMediaDrm.Provider, or DrmSessionManager is changed. setEnableDecoderFallback(true) only affects MediaCodec selection on codec failures, not Widevine CDM security-level negotiation. Retrying an identical DRM configuration after a genuine provisioning failure will most likely reproduce the same error. Combined with finding #1, this means the feature currently has no mechanism to actually achieve L1→L3 degradation, and even if it did, the host app is never given a chance to observe the improved outcome before onError fires.
Fix this →

3. Sentry can no longer distinguish "fallback attempted" from "no fallback" (non-blocking, observability gap)
The PR description states the rootCause tag should be DRM_L3_FALLBACK or DRM_LICENSE_ACQUISITION_FAILED, but SentryLogger.kt:120-127 never sets "DRM_L3_FALLBACK" anywhere — every DRM-related error (whether or not attemptDrmFallback() ran) is tagged DRM_LICENSE_ACQUISITION_FAILED. This isn't a regression versus pre-PR behavior, but it does mean the intended observability improvement (distinguishing "we tried to fall back and it still failed" from "no fallback logic engaged") isn't actually realized. Worth passing a flag into logPlaybackException if this distinction is still wanted.

Regression Assessment

  • DRM/Playback: Compiles cleanly by inspection (all round-1 compile errors remain fixed). The feature itself is currently a no-op with respect to its stated purpose (chore: Configure Renovate #1, build: add Maven Central publishing configuration #2) — no regression to existing non-DRM playback paths, but the feature does not deliver graceful degradation yet.
  • Observability: rootCause/widevine_security_level tagging is safe and non-regressive versus pre-PR behavior; the DRM_L3_FALLBACK distinction described in the PR body isn't implemented (feat: Add TPStreamsPlayer events and methods #3).
  • Other areas (live playback, offline downloads, seeking, track selection, audio focus, background playback, existing public APIs): unaffected — no changes touch those paths beyond the shared onPlayerError handler covered above.

Final Verdict

CHANGES REQUESTED

The compile-breaking and error-swallowing issues from earlier rounds are resolved, but the fix for the swallowed-error bug (removing the early return) had the side effect of making the fallback attempt fully invisible to the host app — onError now fires unconditionally for every DRM provisioning error (#1), and the underlying re-prepare still doesn't force L3 decryption (#2). As it stands, this PR changes internal behavior (an extra background retry + Sentry tag) without changing what the integrating app observes, so it doesn't yet deliver the graceful-degradation behavior described in the PR title/description.
(branch: feat-L3-fallback-DRM-issue)

@testpress testpress deleted a comment from claude Bot Aug 15, 2026
@Balamurugan-100
Balamurugan-100 force-pushed the feat-L3-fallback-DRM-issue branch 2 times, most recently from 113e0d7 to 4ca8446 Compare August 15, 2026 18:20
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @Balamurugan-100's task in 10s —— View job


I'll analyze this and get back to you.

@Balamurugan-100

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @Balamurugan-100's task in 5m 19s —— View job


Claude finished reviewing

Todo list

  • Gather context (read changed files, diff against origin/main)
  • Understand the request (round-5 code review of DRM L3-fallback PR)
  • Review TPStreamsPlayer.kt changes
  • Review WidevineDrmSessionManagerProvider.kt / WidevinePlaybackLevelResolver.kt / WidevinePlaybackLevel.kt
  • Review SentryLogger.kt / DownloadController.kt / DecoderInfoProvider.kt changes
  • Check whether prior round findings (ANR risk, transient-failure persistence, CryptoException swallowing) are addressed
  • Post final review summary and verdict

Summary

This round is a substantial improvement over rounds 1–4, and I believe it resolves every previously-flagged blocking issue. I was again unable to run ./gradlew compileDebugKotlin in this sandboxed environment (network/gradle daemon access is blocked), so compile-correctness is based on careful static reading against my knowledge of the Media3 1.8.1 API (version pinned in gradle/libs.versions.toml:11), not an actual build log — flagging this so it can still be double-checked in CI.

Confirmed resolved since round 4:

  • Blocking-network-call / ANR risk (round 4 chore: Configure Renovate #1): WidevinePlaybackLevelResolver.initialize() (WidevinePlaybackLevelResolver.kt:59-101) now defaults immediately to native level and launches the provisioning probe on CoroutineScope(Dispatchers.IO) — no more runBlocking on the caller's thread.
  • Transient-vs-permanent conflation (round 4 build: add Maven Central publishing configuration #2): runProvisioningProbe() now returns a 3-state ProvisioningProbeResult (WidevinePlaybackLevelResolver.kt:215-267) and only PERMANENT_FAILURE (HTTP 4xx / DeniedByServerException) persists L3; timeouts/5xx/IO errors are TRANSIENT_FAILURE and don't touch SharedPreferences.
  • CryptoException swallowing on streaming content (round 4 feat: Add TPStreamsPlayer events and methods #3): the old isDrmLicenseExpiredError() that unconditionally routed DISALLOWED_OPERATION/SYSTEM_ERROR/CryptoException to the no-op-for-streaming renewDrmLicense() is gone. It's now correctly gated on error.errorCode == ERROR_CODE_DRM_LICENSE_EXPIRED && isDownloadedAsset(assetId) (TPStreamsPlayer.kt:385-389) — this is actually a fix over pre-existing main behavior, not just this PR's own earlier rounds.
  • No real L3-forcing mechanism (round 4 build: add Maven Central publishing configuration #2, rounds 1–3): replaced with a real WidevineDrmSessionManagerProvider (WidevineDrmSessionManagerProvider.kt) installed via DefaultMediaSourceFactory.setDrmSessionManagerProvider(...) (TPStreamsPlayer.kt:1049-1055), which builds a DefaultDrmSessionManager backed by a FrameworkMediaDrm with securityLevel="L3" set explicitly — this mirrors DefaultDrmSessionManagerProvider's real internal construction (headers, multiSession, keySetId all forwarded consistently), so round 4 finding feat: Add non-drm download support #5 (inconsistent config between two divergent code paths) is also moot since there's now one path.
  • Errors invisible/always-visible to host app (round 3 chore: Configure Renovate #1, round 2): onPlayerError's DRM block (TPStreamsPlayer.kt:408-432) now only suppresses the error on the first fallback attempt (!shouldForceL3()), via retryPlaybackAfterL3Persist(). Once shouldForceL3() is already true, a repeat DRM failure falls through to the normal SentryLogger/_listener?.onError(...) path — so the host is told exactly once the fallback itself has failed, rather than never or always.
  • Sentry rootCause mislabeling (round 1 feat: Add events and control methods for TPStreamsPlayer #4): now correctly gated on error.errorCodeName?.contains("DRM") (SentryLogger.kt:116-119), not applied to unrelated errors.

Findings (all non-blocking)

1. Offline downloaded DRM content bypasses the L3 resolution entirely (worth verifying on-device)
DownloadController.kt:568,699 acquire offline licenses via OfflineLicenseHelper.newWidevineInstance(...), which is independent of WidevinePlaybackLevelResolver/WidevineDrmSessionManagerProvider and always uses the native/default CDM. But playback of that same downloaded content (TPStreamsPlayer.kt:609-642, playFromDownload) goes through the same exoPlayer/DefaultMediaSourceFactory as streaming, i.e. the new WidevineDrmSessionManagerProvider. If shouldForceL3() becomes true (session-scoped or persisted) after a license was already acquired under the native/default CDM, playback would decrypt that keySetId through a differently-configured (securityLevel=L3) FrameworkMediaDrm instance than the one that acquired it — Widevine license grants/policies are tied to the security level of the acquiring CDM, so this combination is untested territory and could fail to decrypt. Worth a manual test: force L3 (e.g. via the persisted-flag path), then play back a previously-downloaded asset that was licensed under native L1.

2. Dead code with a latent bug: resolvePlaybackLevel() is unused and ignores its own parameter
WidevinePlaybackLevelResolver.kt:168-188 — this function isn't called anywhere (initialize() reimplements the same logic inline at lines 59-101), and no test file references it. It also silently ignores its nativeWidevineLevel parameter, reading the object's private nativeLevel field instead (WidevinePlaybackLevelResolver.kt:174: if (forceL3Persisted || nativeLevel == "L3"), not nativeWidevineLevel == "L3"). Either delete it, or if it was meant to make this logic unit-testable, wire it into initialize() and fix the parameter shadowing.
Fix this →

3. Minor duplication: native Widevine level is queried twice via separate MediaDrm instances
WidevinePlaybackLevelResolver.readNativeWidevineLevel() (WidevinePlaybackLevelResolver.kt:194-213) and DecoderInfoProvider.collectWidevineLevel() (DecoderInfoProvider.kt:111-123) each independently open/query/close their own MediaDrm(WIDEVINE_UUID) to read the same "securityLevel" property. DecoderInfoProvider.buildTags/buildContext could just call WidevinePlaybackLevelResolver.getNativeWidevineLevel() instead of maintaining a second MediaDrm session. Low impact (each is lazy/cached, so it only happens once per process), but worth consolidating to a single source of truth.

4. Unused import
TPStreamsPlayer.kt:4 (import android.media.MediaCodec) is now only referenced inside a comment (TPStreamsPlayer.kt:406) — the actual MediaCodec.CryptoException check moved into WidevinePlaybackLevelResolver.isDrmFallbackError(). Lint-level nit, not a compile error.

5. Live-stream retry path re-seeks to the pre-error position
retryPlaybackAfterL3Persist() (TPStreamsPlayer.kt:487-500) always calls exoPlayer.seekTo(currentPosition) after re-preparing, for both VOD and live content. The existing generic retryPlayback() (TPStreamsPlayer.kt:180-199) does not seek at all. Worth confirming this is intentional for live DRM streams — reseeking to a stale absolute position after a fresh prepare() on a live timeline can behave differently than resuming at the live edge.

Regression Assessment

  • DRM/Playback: Compiles cleanly by inspection; the L3-forcing mechanism and error-visibility semantics are now both correctly implemented and consistent with the PR's stated goal of graceful L1→L3 degradation. Main residual risk is offline-download/streaming CDM-security-level mismatch (chore: Configure Renovate #1), which is architecturally unchanged risk from round 4, not something this round introduced.
  • Observability: Sentry tagging (rootCause, widevine_security_level, new widevine_playback_level) is correctly scoped and non-regressive vs. pre-PR main.
  • Other areas (live playback path aside from feat: Add non-drm download support #5, offline downloads aside from chore: Configure Renovate #1, seeking, track selection, audio focus, background playback, existing public APIs): no other changes touch these paths.

Final Verdict

Looks good — no blocking issues found. All compile-breaking and logic-breaking issues from rounds 1–4 appear resolved, and the core mechanism (custom ExoMediaDrm.Provider forcing securityLevel=L3 via a shared DrmSessionManagerProvider) is architecturally sound. The remaining items (#1#5) are non-blocking: #1 is worth a manual on-device test before shipping to users with offline downloads; #2#5 are minor cleanup.
(branch: feat-L3-fallback-DRM-issue)

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @Balamurugan-100's task in 2m 6s —— View job


Claude finished reviewing

Todo list


Summary

This commit (7b36788, "refactor") is a follow-up to the round-5 review and only touches the 5 non-blocking items flagged there. No new functional surface is added. I reviewed the delta between 4ca8446 (round-5-reviewed state) and 7b36788 (current HEAD).

All 5 round-5 findings are now resolved:

  1. Offline downloads bypassing L3 resolution (round 5 chore: Configure Renovate #1)WidevineDrmSessionManagerProvider.createOfflineLicenseHelper() (new, WidevineDrmSessionManagerProvider.kt:93-115) now checks WidevinePlaybackLevelResolver.shouldForceL3() and, when true, builds the OfflineLicenseHelper from a DefaultDrmSessionManager using the same L3_EXO_MEDIA_DRM_PROVIDER (securityLevel=L3) that playback uses, instead of always going through OfflineLicenseHelper.newWidevineInstance() (native/default CDM). Both call sites in DownloadController.kt (license download at line 569, license renewal at line 699) now route through this helper, so offline license acquisition and playback are consistent about which security level's CDM is used. This closes the CDM-security-level-mismatch risk flagged previously.

  2. Dead code / shadowed parameter (round 5 build: add Maven Central publishing configuration #2)WidevinePlaybackLevelResolver.resolvePlaybackLevel() (unused, and it silently ignored its nativeWidevineLevel parameter in favor of the private nativeLevel field) has been deleted entirely. initialize() remains the single, correct implementation.

  3. Duplicate MediaDrm queries (round 5 feat: Add TPStreamsPlayer events and methods #3)DecoderInfoProvider.widevineLevel no longer opens its own MediaDrm(WIDEVINE_UUID) session; it now delegates to WidevinePlaybackLevelResolver.getNativeWidevineLevel() (DecoderInfoProvider.kt:22-23). Verified nativeLevel is populated synchronously inside WidevinePlaybackLevelResolver.initialize() (WidevinePlaybackLevelResolver.kt:68, not part of the async provisioning-probe coroutine), and DecoderInfoProvider.widevineLevel is only read from Sentry tag builders (buildTags/buildContext), which are only invoked after player/DRM initialization has already run — so there's no ordering hazard from the switch away from by lazy.

  4. Unused import (round 5 feat: Add events and control methods for TPStreamsPlayer #4)import android.media.MediaCodec removed from TPStreamsPlayer.kt.

  5. Live-stream re-seek after L3 retry (round 5 feat: Add non-drm download support #5)retryPlaybackAfterL3Persist() (TPStreamsPlayer.kt:486-501) now guards the exoPlayer.seekTo(currentPosition) call with if (!_isLiveStream), consistent with how live-stream state is checked elsewhere in the same file (e.g. line 362). VOD retains its resume-position behavior; live streams now resume at the live edge instead of a stale absolute position.

New-code check: createOfflineLicenseHelper()'s L3 branch manually constructs DefaultDrmSessionManager + OfflineLicenseHelper(drmSessionManager, eventDispatcher), mirroring the internal construction OfflineLicenseHelper.newWidevineInstance() already does for the default path — no explicit setMode(...) call is needed at construction since downloadLicense/renewLicense set the mode internally, consistent with the existing default-path helper. Header propagation is unchanged from the prior behavior (headers flow via the DataSource.Factory passed in at both call sites, same as before this commit).

I was unable to run ./gradlew compileDebugKotlin in this sandboxed environment (network/build-daemon access is blocked), so this is based on careful static reading against the Media3 API, not an actual build log.

Regression Assessment

  • DRM/Playback: No change in behavior beyond what's described above; the L3-forcing mechanism validated in round 5 is untouched. Offline-download/playback CDM consistency is now improved (closes round-5 finding chore: Configure Renovate #1).
  • Live playback: Fixed to no longer re-seek to a stale position after an L3-triggered retry.
  • Offline downloads: License acquisition now correctly respects the resolved/forced playback level.
  • Observability: No changes since round 5.
  • Other areas: No changes touch seeking (for VOD), track selection, audio focus, background playback, or existing public APIs.

Final Verdict

APPROVED

All previously-flagged blocking issues (rounds 1–4) and non-blocking cleanup items (round 5) are now resolved. No new issues were introduced by this refactor commit.

@testpress testpress deleted a comment from claude Bot Aug 16, 2026
@testpress testpress deleted a comment from claude Bot Aug 16, 2026
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

1 similar comment
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@Balamurugan-100

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

2 similar comments
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@Balamurugan-100
Balamurugan-100 force-pushed the feat-L3-fallback-DRM-issue branch from d9f5d07 to a9c46e9 Compare August 16, 2026 05:32
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@Balamurugan-100
Balamurugan-100 changed the base branch from main to network-error August 16, 2026 05:33
@Balamurugan-100 Balamurugan-100 changed the title feat: add DRM L3 fallback resilience feat: add opt-in Widevine L3 fallback for DRM playback failures Aug 16, 2026
Base automatically changed from network-error to main August 16, 2026 05:37
@Balamurugan-100
Balamurugan-100 force-pushed the feat-L3-fallback-DRM-issue branch from a9c46e9 to 4a33570 Compare August 16, 2026 05:37
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@Balamurugan-100
Balamurugan-100 force-pushed the feat-L3-fallback-DRM-issue branch from 4a33570 to 5c2b726 Compare August 16, 2026 06:59
@testpress testpress deleted a comment from claude Bot Aug 16, 2026
@testpress testpress deleted a comment from claude Bot Aug 16, 2026
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@Balamurugan-100

Copy link
Copy Markdown
Member Author

@claude review

@testpress testpress deleted a comment from claude Bot Aug 18, 2026
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown

Claude finished @Balamurugan-100's task in 3m 14s —— View job


Claude finished reviewing

Todo list

  • Gather context (diff against origin/main, identify new commits since the round-5/refactor approval)
  • Review new commits: 96df476 (guard L3 fallback retry / resolver safety), af781bd (fix), 526a35b (fix: Network error)
  • Re-verify prior round findings still hold
  • Check for newly introduced issues
  • Post final review summary and verdict

Summary

This round adds three commits on top of the previously-approved state (round 5 + refactor): 96df476, af781bd, 526a35b. All three are targeted fixes and I did not find any blocking issues. I was unable to run ./gradlew compileDebugKotlin in this sandboxed environment (no approval for network/build-daemon access), so this is a static review, not a build-verified one — same caveat as prior rounds.

What changed and why it's correct:

  1. 96df476 — guards against repeated/duplicate L3 fallback attempts. Adds l3FallbackAttempted (TPStreamsPlayer.kt:155-156) so onPlayerError's DRM block (TPStreamsPlayer.kt:457-461) and retryPlaybackAfterL3Persist() (TPStreamsPlayer.kt:545-549) only trigger one fallback retry per prepared media item, instead of potentially looping on repeated errors. retryPlaybackAfterL3Persist() now also preserves the DRM configuration explicitly via the new buildMediaItemForL3Retry() (TPStreamsPlayer.kt:562-570), aborting the retry (falling through to normal error reporting) rather than silently dropping DRM config if content is DRM-protected but has no drmConfiguration to preserve. WidevinePlaybackLevelResolver changes are safety/consistency hardening: a single initLock now guards allowFallbackToL3/cachedLevel reads+writes together (closing a prior check-then-act race), persistForceL3ToPrefs is now idempotent (avoids a duplicate log/write if already persisted), and the provisioning-probe coroutine moved off an ad hoc CoroutineScope(Dispatchers.IO) onto a shared probeScope (SupervisorJob + Dispatchers.IO) so a probe failure can't silently cancel sibling work. All net improvements, no regressions found.

  2. af781bd — resets l3FallbackAttempted = false on every fresh setMediaItem/prepare() (TPStreamsPlayer.kt:663, 706, 763 — streaming prepare, download playback, and refreshPlaybackWithDownloadMediaItem). This is necessary so a new asset/session isn't permanently locked out of fallback by a flag left over from a previous asset on the same long-lived player instance. I checked the other prepare() call sites (play() at TPStreamsPlayer.kt:775-780, generic retryPlayback() at TPStreamsPlayer.kt:230-243) — these correctly do not reset the flag since they re-prepare the same media item/session rather than starting a new one, so the one-shot-per-asset guarantee from 96df476 is preserved.

  3. 526a35b — fixes the actual root cause of the "Network error" title: the DRM-fallback block was being short-circuited. Previously isNetworkError(error) was checked before the DRM fallback block; since some DRM errors (e.g. LICENSE_ACQUISITION_FAILED, SYSTEM_ERROR) can be wrapped in an IOException cause, they could match isNetworkError() and return early via networkDiagnosticsManager.handleError(...) — meaning the DRM fallback logic added by this PR would never run for those errors. Moving the DRM block before the network check (TPStreamsPlayer.kt:452-490) and additionally excluding DRM's 6000..6999 error-code range from isNetworkError() (NetworkRecoveryHandler.kt:102) closes this both at the call-site ordering level and defensively inside the classifier itself. I confirmed PlaybackException's DRM error codes (ERROR_CODE_DRM_*) all fall in 6000-6999 per the Media3 error-code space, so the exclusion is correctly scoped and won't accidentally swallow genuine network codes (those are in the 2000s range).

Verified prior-round findings are unaffected:

  • The core L3-forcing mechanism (WidevineDrmSessionManagerProvider, gated on WidevinePlaybackLevelResolver.shouldUseL3Drm()) is untouched by these 3 commits and still consistent between streaming (TPStreamsPlayer.kt:1145-1155) and offline license acquisition (WidevineDrmSessionManagerProvider.createOfflineLicenseHelper, used by both DownloadController.kt call sites) — round 5/refactor finding chore: Configure Renovate #1 remains resolved.
  • onPlayerError still reports to the host app exactly once if the fallback itself doesn't resolve things (via l3FallbackAttempted gating re-entry into the suppress-and-retry branch on the second failure) — round 3 finding chore: Configure Renovate #1 remains resolved.

Minor / non-blocking observations

1. retryMediaItem is built from isDrmContent, but the actual retry-abort check in buildMediaItemForL3Retry is scoped to isDrmContent && drmConfiguration == null. In practice this only matters if a DRM-flagged asset's MediaItem can ever end up with localConfiguration != null but drmConfiguration == null — I couldn't find a code path that produces that combination for the streaming/download prepare flows, so this looks like defensive code that's unlikely to trigger, not a bug.

2. LICENSE_ACQUISITION_FAILED triggering L3 fallback doesn't distinguish "genuine network outage during license fetch" from "device/DRM incapability." This is pre-existing behavior from earlier rounds (not introduced by this round's commits), but worth flagging again now that DRM-block ordering changed: a license request that fails purely because of a transient network outage will now consume the one-shot l3FallbackAttempted fallback and retry immediately (likely failing again while the network is still down), rather than going through the network-diagnostics retry/backoff path. Not blocking — the existing behavior across all prior approved rounds already accepted this classification — but worth a manual test of "DRM playback start during a flaky/offline network" if not already covered.

3. Three new public getters were added in 526a35b (getDrmLicenseUrl(), getDrmPlaybackLevel(), getNativeWidevineLevel()TPStreamsPlayer.kt:134-142) that aren't used anywhere in this diff (including the sample apps). They look like intentional API surface for host apps to introspect the resolved DRM level, but since they weren't mentioned in the PR description and aren't exercised by any caller in this branch, it's worth confirming they're actually wanted as public API (vs. leftover debug scaffolding) before merge.

Regression Assessment

  • DRM/Playback: The fallback mechanism is now reachable for the error cases it was designed for (fixes the ordering bug that made it effectively unreachable for IOException-wrapped DRM errors), retries are bounded to one attempt per asset, and DRM config is preserved across the retry. No regressions found in the reviewed commits.
  • Networking: isNetworkError()'s new DRM-code exclusion is additive and narrowly scoped; existing network-error classification for non-DRM codes is unchanged.
  • Other areas (live playback, seeking, track selection, offline downloads, audio focus, background playback, existing public APIs): unaffected by these 3 commits beyond the new getters noted in feat: Add TPStreamsPlayer events and methods #3.

Final Verdict

No blocking issues found. These three commits fix a real bug (DRM fallback being unreachable due to check ordering) and harden the retry/resolver logic against races and repeated attempts. Items #1#3 above are minor and non-blocking — #2 is worth a manual on-device test, #3 is worth a quick confirmation that the new public getters are intentional API surface.
(branch: feat-L3-fallback-DRM-issue)

@testpress testpress deleted a comment from claude Bot Aug 18, 2026
@testpress testpress deleted a comment from claude Bot Aug 18, 2026
@Balamurugan-100
Balamurugan-100 merged commit bd95569 into main Aug 18, 2026
4 of 5 checks passed
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