Video Decoding
Kodik protects direct video URLs with a two-stage obfuscation: a ROT-style substitution cipher followed by URL-safe Base64. The decoder walks an eight-step pipeline to unwrap it, with brute-force across all 26 ROT shifts and a proxy-aware HTTP client underneath.
The eight steps
Section titled “The eight steps”flowchart LR
A["Kodik iframe URL<br/>//kodik.info/seria/..."] --> B["Load iframe HTML"]
B --> C["Extract JS params<br/>urlParams, type, hash, id"]
C --> D["Load player JS<br/>app.player_*.js"]
D --> E["Extract POST URL<br/>atob('...')"]
E --> F["POST /gvi or /ftor<br/>with form data"]
F --> G["Encoded video URLs<br/>in JSON response"]
G --> H["ROT decode<br/>brute-force all 26 shifts"]
H --> I["Base64 decode<br/>URL-safe variant"]
I --> J["Direct MP4 URL<br/>https://p12.solodcdn.com/..."]
- Load the iframe page —
GET https://kodik.info/seria/{id}/..., routed through the proxy pool with a direct fallback. - Extract JS params — regex pulls
urlParams,type,hash,idfrom the embedded script tag. - Load the player JS —
GET /assets/js/app.player_*.js. The exact filename changes, so we extract it from the iframe HTML on each call. - Extract the POST endpoint — inside the player JS we look for an
atob("...")call whose decoded value is the video-info path. We cache the last known path (/gvi,/kor,/ftor,/seria) and fall back to the full chain if it fails. - POST the video-info request —
application/x-www-form-urlencodedwith the four params plusbad_user=false. - Parse the JSON response — the body looks like
{"links":{"720":[{"src":"encoded..."}]}}. - ROT decode — try all 26 shifts. We cache the last working shift; if the cached shift fails, we fall through to the full brute-force loop.
- URL-safe Base64 decode — replace
-→+,_→/, pad with=, thenBase64.getDecoder().decode(...).
Geo-blocked responses
Section titled “Geo-blocked responses”When Kodik refuses to serve the player from the caller’s IP, step 6 parses a
response that carries no real URLs — just placeholder strings like "true".
parseVideoResponse detects this pattern and returns an empty quality
map: no sentinel keys, no sentinel values. Downstream consumers
(ParserService.selectBestQuality, VideoDownloadService.pickBestQualityUrl,
StreamController.pickBestQuality) additionally drop any key starting with
_ and any value that does not start with http, so a future sentinel that
escapes the decoder cannot leak into mp4_link.
Why brute-force
Section titled “Why brute-force”KodikDownloader (one of the reference projects) hard-codes ROT +18. That
works today, but has failed on earlier Kodik updates. Brute-force survives
shift changes without a deploy; the overhead is a handful of integer
comparisons per decode, which is free compared to the network cost.
Proxy and retry
Section titled “Proxy and retry”Every outbound HTTP call goes through ProxyWebClientService, which picks a
proxy from kodik_proxy using round-robin and retries against direct if the
proxy fails. The decoder itself is wrapped in Retry.backoff(maxRetries, 2s),
so a transient 5xx or network error triggers exponential backoff (2s, 4s,
8s).
Caching
Section titled “Caching”- ROT shift — the last successful shift is cached in-memory.
- POST endpoint — the last working endpoint is cached per player JS fingerprint.
Both caches are process-local; a restart re-learns them from the first live decode.