# Using the capture binary `drm capture` runs an HTTPS MITM **on the phone**, drives the app, and records the DRM exchange. It is both the production capture path and the tool you use to discover everything a new module needs. ## Device setup (once) ```bash adb devices # phone listed and authorised ./bin/drm proxy build # cross-compile the on-device MITM (linux/arm64) ./bin/drm proxy install-ca --reinject ``` The phone must be rooted (Magisk). `install-ca` pushes the MITM CA and bind-mounts it over the system trust store, including into already-running zygote namespaces — a user-store CA is not enough, modern apps ignore it. Verify the mount: ```bash adb shell 'ls /system/etc/security/cacerts/ | grep 6c3578b4' adb shell 'su -c "ls /apex/com.android.conscrypt/cacerts/6c3578b4.0"' ``` If an app still reports `tls: unknown certificate`, re-run the reinject and force-stop the app so it inherits the mount: ```bash ./bin/drm proxy reinject-ca adb shell am force-stop ``` Keep the screen on for the whole session — UI automation cannot tap a dark screen: ```bash adb shell svc power stayon true adb shell settings put system screen_off_timeout 1800000 ``` Restore the timeout afterwards (default is usually 60000). ## Three modes ### Passive — no module needed ```bash ./bin/drm capture --wait 300 ``` Starts the MITM and nothing else. You open any app and play something; it records whatever license, PSSH and manifest traffic appears and writes `outputs/capture//`. This is the first thing to run against an unknown app. ### Launch only ```bash ./bin/drm capture --app myapp --wvd data/device.wvd ``` Launches the app, then waits while you navigate by hand. Useful while you are still working out the UI sequence. ### Full automation ```bash ./bin/drm capture --app myapp --channel main --auto-play --wvd data/device.wvd ``` Runs the module's `Launch` and `AutoPlay`, then waits for the required fields. ## Flags | Flag | Default | Purpose | |---|---|---| | `--app` | _(empty)_ | module to drive; empty means passive | | `--channel` | — | channel id, required by `--auto-play` | | `--auto-play` | false | run the module's navigation | | `--wait` | 180 | seconds to wait for the capture to complete | | `--wvd` | — | Widevine device file; required to fetch keys | | `--python` | `.venv` | interpreter for `wvkey.py` | | `--key-mode` | from the module | force `modulardrm` or `raw` | | `--user-agent` | — | UA for the license request | | `--serial` | sole device | target a specific phone | | `--close` | true | force-stop the app when done | | `--.` | — | override any of that module's values | ## What a run looks like ```text [*] Stopping any old appproxy... [*] Pushing appproxy... [+] appproxy pid=7604; capture -> /data/local/tmp/appproxy_cap.json [*] Setting HTTP proxy -> 127.0.0.1:8080 (was "null") [*] Re-injecting system CA (Magisk)... [+] System CA present in conscrypt [*] Launching rte (air.RTE.OSMF.Minimal)… [*] Auto-play RTE 2… [*] tap_ui "Live"/"Live tab, 2 out of 5" @ 324,2253 [*] tap chip @ 356,297 [*] tap Play @ 540,720 [+] air.RTE.OSMF.Minimal is PLAYING [*] Waiting for license + PSSH + manifest… [+] Capture: pid: RKRPCEib8z9V mpd: https://dai.google.com/linear/dash/pa/event/.../stream/... license: https://widevine.entitlement.eu.theplatform.com/wv/web/ModularDrm?... pssh: AAAASnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACoiIGRmMTYzMzgyMWRk… [+] key: 5490781a77572107ec251237e94e3b53:cee5fc45b70040ffea2b2576fb5abda5 [+] session: outputs/rte/20261005-220252/session.json [*] Closing air.RTE.OSMF.Minimal… [*] HTTP proxy cleared ``` ### Output ```text outputs/// session.json everything, machine-readable mpd.txt manifest URL key.txt KID:KEY (one per line for multi-key streams) pssh.txt Widevine init data auth.txt Authorization value, when the app uses one pid.txt provider id, when the app uses one outputs//latest/ a copy of the most recent run ``` Live log while a capture runs: `.cache/appproxy-.log`. --- ## Discovering a new app ### 1. Record everything ```bash ./bin/drm proxy discover --install-ca --reinject # play the app on the phone; Ctrl+C when done ``` Writes `outputs/discover//`: | File | Contents | |---|---| | `appproxy_traffic.jsonl` | every HTTP(S) request/response, one JSON object per line | | `appproxy_cap.json` | the structured fields the proxy recognised | | `appproxy.log` | tagged lines: `[MPD]`, `[LIC]`, `[PSSH]`, `[MAN]` | ### 2. Mine it ```bash # the android package id adb shell dumpsys window | grep mCurrentFocus # license endpoint grep -iE 'license|licence|widevine|/lic/' outputs/discover/*/appproxy.log # manifest candidates grep -oE 'https?://[^"]+\.(m3u8|mpd)' outputs/discover/*/appproxy_traffic.jsonl \ | sort -u # the hosts involved, by frequency grep -oE '"host":"[^"]+"' outputs/discover/*/appproxy_traffic.jsonl \ | sort | uniq -c | sort -rn | head -20 # config blobs that often carry ids and tokens grep -oE 'https?://[^"]+(config|playback)[^"]*\.json' \ outputs/discover/*/appproxy_traffic.jsonl | sort -u ``` What to pull out, and where it goes in `module.yaml`: | Found | Key | |---|---| | package id | `package` | | license endpoint | `license_url` | | CDN host serving the manifest | `score.hosts` | | analytics / EPG hosts polluting the results | `score.deny` | | account ids, policy keys, video ids | provider-specific keys | ### 3. Work out the UI Dump the tree at each step and read off stable selectors: ```bash adb shell uiautomator dump /sdcard/ui.xml adb shell cat /sdcard/ui.xml > ui.xml ``` Extract the useful attributes: ```bash grep -oE 'resource-id="[^"]*"' ui.xml | sort -u grep -oE 'content-desc="[^"]*"' ui.xml | sort -u ``` Prefer `resource-id` over `content-desc` or `text` — ids survive translation and copy changes. Check a candidate marker really exists before you rely on it; a launch marker that never matches costs the full timeout on every capture and is easy to miss because the pipeline recovers anyway. Test taps by hand before writing Go: ```bash adb shell input tap 540 732 adb shell input swipe 540 1700 540 700 350 ``` ### 4. Determine the key mode | The app sends | Use | |---|---| | a JSON wrapper around the challenge, with an Authorization header | `modulardrm` | | a raw binary challenge, `Content-Type: application/octet-stream` | `raw` | Check the license request body in `appproxy_traffic.jsonl`. Then set `KeyMode()` in the module — never sniff it from the URL at runtime. ### 5. Decide which fields are required Look at what `appproxy_cap.json` actually contained after a successful play: - DASH + JSON license → usually `auth`, `pid`, `pssh`, `mpd` - HLS + binary license → usually `license_url`, `mpd` (the PSSH comes from the playlist afterwards) Pass exactly those to `a.Hints(...)`. Asking for a field the app never emits makes every capture time out. ### 6. Verify Where an app has a public catalog, capture the same channel both ways and compare — independent paths agreeing on the PSSH and key set is strong evidence both are right: ```bash ./bin/drm catalog --app myapp --channel main --keys --wvd data/device.wvd --json > a.json ./bin/drm capture --app myapp --channel main --auto-play --wvd data/device.wvd diff <(jq -r '.keys[]' a.json | sort) <(sort outputs/myapp/latest/key.txt) ``` Also sanity-check that the KID you captured matches whatever you recorded in `module.yaml` for that channel. ## Troubleshooting | Symptom | Likely cause | |---|---| | `tls: unknown certificate` in the app | CA not mounted into conscrypt; reinject, then force-stop the app | | `timed out waiting for capture ([auth pid pssh mpd])` | playback never started, or `Hints` asks for a field this app does not send | | `launch: ui node not found within Ns` | launch marker does not match — dump the tree and pick a resource id | | `launch: exit status 1` | monkey raced a force-stop; usually transient, retry | | `auto-play failed: ... not found` | UI changed, or the phone is on a different screen than expected | | manifest field holds an EPG or config URL | add that host to `score.deny` | | `CA inject skipped/failed` | transient after repeated runs; re-run `proxy reinject-ca` | | Screen asleep mid-run | `adb shell svc power stayon true` | Clear the phone's proxy if a run is interrupted: ```bash ./bin/drm proxy clear-proxy ```