Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ZemerTeam/zemer-cipher/llms.txt

Use this file to discover all available pages before exploring further.

On low-RAM devices under sustained memory pressure, the Android OS can OOM-kill the WebView renderer process. RendererRecoveryPolicy tracks consecutive renderer deaths and opens a short backoff window after repeated failures, preventing the library from wasting time re-parsing the ~2.8 MB player JS when a fresh renderer would immediately be killed again. Package: com.zemer.cipher The policy is pure Kotlin with no Android dependencies, making it fully unit-testable with any monotonic clock source.

Constructor

class RendererRecoveryPolicy(
    maxConsecutiveFailures: Int = DEFAULT_MAX_CONSECUTIVE_FAILURES, // 3
    backoffMs: Long = DEFAULT_BACKOFF_MS                            // 60_000L
)
maxConsecutiveFailures
Int
Number of consecutive renderer deaths that must occur before the backoff window is armed. Defaults to DEFAULT_MAX_CONSECUTIVE_FAILURES (3). After this many consecutive failures, shouldAttempt returns false until the backoff window expires.
backoffMs
Long
Duration of the backoff window in milliseconds. Defaults to DEFAULT_BACKOFF_MS (60 000 ms = 1 minute). The window starts when consecutiveFailures reaches maxConsecutiveFailures inside onFailure.

Companion Constants

ConstantValueDescription
DEFAULT_MAX_CONSECUTIVE_FAILURES3Default threshold of consecutive renderer deaths before backoff.
DEFAULT_BACKOFF_MS60_000LDefault backoff window duration (1 minute).

Properties

consecutiveFailures

var consecutiveFailures: Int
    private set
The current count of consecutive renderer deaths. Read-only from outside the class. Incremented by onFailure; reset to 0 by onSuccess.

Methods

shouldAttempt

fun shouldAttempt(nowMs: Long): Boolean
Returns true if creating or using a WebView should be attempted right now. Returns false only when both of the following are true:
  • consecutiveFailures >= maxConsecutiveFailures (the failure threshold has been reached), and
  • the current time nowMs is still within the backoff window (i.e. nowMs < backoffUntilMs).
Once the backoff window expires, this method returns true again, allowing exactly one attempt. If that attempt fails, onFailure re-arms the window immediately.
nowMs
Long
required
Current monotonic clock value in milliseconds. Pass SystemClock.elapsedRealtime() in production code.

onFailure

fun onFailure(nowMs: Long)
Records a renderer death (or a renderer-gone-equivalent timeout). Increments consecutiveFailures. When consecutiveFailures reaches maxConsecutiveFailures, arms the backoff window by setting backoffUntilMs = nowMs + backoffMs.
nowMs
Long
required
Current monotonic clock value in milliseconds at the time of the failure. Pass SystemClock.elapsedRealtime() in production code.

onSuccess

fun onSuccess()
Records a successful WebView operation. Fully resets the policy: sets consecutiveFailures = 0 and clears the backoff window (backoffUntilMs = 0). Call this after any successful sig deobfuscation or n-transform completes without a CipherRendererGoneException.

Usage Example

val policy = RendererRecoveryPolicy()

suspend fun decipher(sig: String): String? {
    val now = SystemClock.elapsedRealtime()
    if (!policy.shouldAttempt(now)) {
        // Backoff active — skip the expensive WebView rebuild.
        return null
    }
    return try {
        val result = cipherWebView.deobfuscateSignature(sig)
        policy.onSuccess()
        result
    } catch (e: CipherRendererGoneException) {
        policy.onFailure(SystemClock.elapsedRealtime())
        null
    }
}
The backoff window is deliberately short (1 minute by default) and half-open: once the window expires, one attempt is always allowed regardless of consecutiveFailures. This design keeps the cipher WebView as the primary deciphering path, since fallback clients (NewPipe extractor, alternate YouTube clients) are generally less reliable. A single successful attempt fully resets the policy; another failure re-arms the window immediately.
Always pass a monotonic clock value such as SystemClock.elapsedRealtime() rather than a wall-clock value such as System.currentTimeMillis(). Monotonic clocks are immune to NTP corrections and manual time changes, which means a backward clock adjustment cannot unexpectedly extend or reset the backoff window while playback is already broken.

Build docs developers (and LLMs) love