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.

YouTube rotates its player_ias JavaScript every few days. Each rotation introduces new, freshly obfuscated function names for the signature cipher and n-transform. Without updated deobfuscation parameters, every stream attempt fails until a new APK is released — which could take days or weeks to reach users. Zemer Cipher’s player config system stores the per-player parameters (sig expression, nClass identifier, and signatureTimestamp) in a single JSON file that is kept up-to-date at runtime, without any app update required.

Config Layers

The config table is built from three layers, applied in order: 1. Bundled asset (library/src/main/assets/player_configs.json) Baked into the APK at build time. Available immediately on cold start, before any network request has completed. Guarantees that apps work offline and on first launch with the player versions that were current when the APK was built. 2. Remote fetch PlayerConfigStore fetches player_configs.json from:
https://raw.githubusercontent.com/ZemerTeam/zemer-cipher/master/library/src/main/assets/player_configs.json
The TTL is 6 hours, matching the player JS cache TTL. Responses include an ETag header; subsequent requests send If-None-Match so a 304 response skips re-parsing the file entirely. Remote entries overlay the bundled table: remote wins per key, bundled-only keys survive. 3. Disk cache The last successfully validated remote response is written atomically (temp-file + rename) to cipher_cache/configs_remote.json. On the next app start, PlayerConfigStore.initialize() loads both the bundled asset and this cached file before any network request, so the most recently fetched config survives app restarts and brief offline periods.

Config Entry Shape

Each entry in player_configs.json follows this structure:
"445213fb": {
  "sig": "mP(4,155,INPUT)",
  "nClass": "Yx",
  "sts": 20613,
  "aliases": ["d62bd338"]
}
FieldDescription
keyThe 8-hex player hash extracted from the player JS URL (/s/player/{hash}/). This is the primary lookup key.
sigThe signature deobfuscation call expression. INPUT is the placeholder for the obfuscated signature argument. Format is locked to name(int,int,INPUT).
nClassThe identifier of the URL class used to construct the n-transform IIFE. The actual IIFE is built locally from a fixed template in PlayerConfigParser.buildNJsExpression() — the remote config supplies only this identifier.
stsThe signatureTimestamp to include in InnerTube /player requests. Must match the value embedded in this player’s JS.
aliasesOne or more alternative hashes for the same player entry. The alias is an MD5 of the first 10,000 bytes of the player JS — a fallback for cases where the URL-based hash cannot be extracted.

Self-Healing Flow

When a player rotation breaks deciphering, the fix propagates to deployed apps without requiring an APK update:
  1. A player rotation causes FunctionNameExtractor to get a cache miss — no config entry exists for the new player hash.
  2. CipherDeobfuscator.getOrCreateWebView() detects incomplete extraction (sig or n-function info is missing) and immediately calls PlayerConfigStore.forceRefresh(missingHash). This is a rate-limited, single-flight network fetch — concurrent misses coalesce into one request, and repeated calls within the 5-minute cooldown window are skipped to protect the config host from repeat hits.
  3. If the new player entry has been pushed to master on GitHub, the fetch brings it in. configEpoch advances, the WebView is rebuilt using the validated config, and deciphering succeeds on the next attempt — without any user action or app update.
  4. If the CDN rejects a deciphered stream with a 403, CipherDeobfuscator.onStreamRejected() triggers PlayerConfigStore.refreshAfterStreamRejection(). Unlike the hash-miss path, this re-fetches even when the current player hash is already present in the table, because the entry may be present but wrong (e.g. a stale config that was correct for a previous sub-rotation). This path has its own independent cooldown so it can never starve the hash-miss recovery path.

Security Model

PlayerConfigParser is the validation boundary. Every value is regex-locked before it enters the in-memory config table:
ValueRegexWhat it prevents
sig^[A-Za-z0-9$_]{1,8}\(\d+,\d+,INPUT\)$Arbitrary JS in the cipher WebView
nClass^[A-Za-z0-9$_]{1,8}$Arbitrary JS in the n-transform expression
Hash keys^[a-f0-9]{8}$Malformed or spoofed lookup keys
Additional file-level rules enforced by PlayerConfigParser.parse():
  • Duplicate hashes or aliases cause the entire file to be rejected — not just the affected entries. A duplicate makes the table ambiguous (iteration-order-dependent), which is a file-level defect.
  • Invalid individual entries are skipped; the rest of the file is used. One bad entry does not discard valid entries.
  • Unsupported schemaVersion causes the entire file to be rejected. Apps built with an older library will never process a file with a higher schema version. They continue running from their last-good table until a library update ships.

Schema Version

schemaVersion must be a non-string integer greater than zero. The current supported version is 1. Bump schemaVersion only on breaking shape changes — that is, changes that would require older readers to be updated before they can safely parse the new format. Bumping on additive changes (new optional fields) is unnecessary and harmful: it immediately breaks all deployed apps that have not yet updated their library. Apps on older library versions that receive a file with a newer schemaVersion silently fall back to their last-good cached or bundled table. They continue working; they simply do not benefit from new entries until their library is updated.
Never manually push a player_configs.json with duplicate hashes or aliases. The parser rejects the entire file on any collision, leaving all deployed apps on their previous (possibly outdated) config table until a corrected file is pushed. Run the unit tests locally before every push.
After adding a new player config entry, run ./gradlew :library:testDebugUnitTest to validate the file locally before pushing to master. The test suite shares the same config-parity/ fixtures as the zemer-app validation harness, so a file that passes tests will parse identically in both readers.

Build docs developers (and LLMs) love