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.

This guide walks you through adding Zemer Cipher to an Android project, initializing the library at app startup, deciphering a YouTube stream URL (signature deobfuscation + n-parameter transformation), and generating a BotGuard PoToken for web client streams. By the end you will have a fully working playback pipeline that self-heals across YouTube player rotations.
1

Add the dependency

Zemer Cipher is published via maven-publish. Declare the dependency in your app module’s build.gradle.kts and make sure google() and mavenCentral() are listed in your project’s dependency resolution repositories:
// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}
// app/build.gradle.kts
dependencies {
    implementation("com.zemer:cipher:1.0.0")
}
Sync your project after saving.
2

Initialize in Application class

Call ZemerCipher.initialize() once during Application.onCreate(). Initialization sets up the shared OkHttpClient, starts a background player-config refresh, and prepares the CipherDeobfuscator context. All other library calls will throw if this step is skipped.
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        ZemerCipher.initialize(
            context = applicationContext,
            proxy = null,          // optional
            debugLogging = BuildConfig.DEBUG
        )
    }
}
Register MyApplication in your AndroidManifest.xml:
<application
    android:name=".MyApplication"
    ... >
3

Decipher a stream URL

All CipherDeobfuscator methods are suspend functions. Call them from a coroutine scope — for example, inside a viewModelScope.launch block or a repository function running on Dispatchers.IO.
// In a coroutine scope:

// 1. Get the signatureTimestamp to include in /player requests.
//    Always use THIS value — it is tied to the player JS the library
//    will actually use for deciphering, avoiding CDN 403s from mismatched players.
val sts = CipherDeobfuscator.signatureTimestamp()

// 2. Deobfuscate a signatureCipher string (the "s=...&sp=...&url=..." query string
//    returned by the InnerTube /player response for web-client adaptive formats).
val streamUrl = CipherDeobfuscator.deobfuscateStreamUrl(
    signatureCipher = signatureCipherParam,
    videoId = videoId
) ?: error("Deobfuscation failed")

// 3. Transform the n-parameter to avoid CDN throttling.
//    Always call this after deobfuscation — even URLs that do not need
//    sig deciphering still benefit from the n-transform.
val finalUrl = CipherDeobfuscator.transformNParamInUrl(streamUrl)
4

Generate a PoToken (web client streams)

Web client (WEB_REMIX, WEB) streams increasingly require a BotGuard PoToken. PoTokenGenerator manages a dedicated WebView that mints tokens for a given session.
val generator = PoTokenGenerator()
val result = generator.getWebClientPoToken(
    videoId = videoId,
    sessionId = visitorData  // visitorData from the InnerTube /player response
)
if (result != null) {
    // Send result.playerRequestPoToken in the /player request body
    // (it is bound to the visitorData session).
    val playerPoToken = result.playerRequestPoToken

    // Append result.streamingDataPoToken as the `pot=` query parameter on
    // the stream URL (it is bound to the video ID).
    val streamUrlWithPot = "$finalUrl&pot=${result.streamingDataPoToken}"
}
The two tokens have different bindings by design: playerRequestPoToken is session-bound (safe to reuse across videos in the same session) while streamingDataPoToken is video-bound (must be regenerated per video). PoTokenGenerator handles this automatically.
5

Prewarm (optional)

On first playback the library fetches ~2.8 MB of player JS and loads it into a WebView, which takes 2–5 seconds on a typical device. Call CipherDeobfuscator.prewarm() in a background coroutine shortly after ZemerCipher.initialize() to absorb that latency before the user requests playback:
// In Application.onCreate(), after ZemerCipher.initialize():
applicationScope.launch(Dispatchers.IO) {
    CipherDeobfuscator.prewarm()
}
prewarm() is guarded by the same mutex as deobfuscateStreamUrl and transformNParamInUrl, so it cannot race a real request. If it fails for any reason the WebView is created lazily on first use instead.
All CipherDeobfuscator methods — signatureTimestamp(), deobfuscateStreamUrl(), transformNParamInUrl(), prewarm(), and onStreamRejected() — are suspend functions. They must be called from a coroutine or another suspend function; calling them from a regular thread will not compile.
Call CipherDeobfuscator.onStreamRejected() when the CDN returns a 403 on a deciphered stream URL. A wrong player config can produce a signature that the player JS computes without throwing — making the error invisible to the library’s internal retry. onStreamRejected() triggers a rate-limited remote config refresh; if the config table changes, the next decipher rebuilds the WebView from the corrected config and recovers playback without restarting the app.

Build docs developers (and LLMs) love