Null-DRM-Official/docs/providers/rte.md
404errordeveloper 2fa8f2435f Initial commit: Null DRM Official
Capture, decrypt, and restream toolkit with compiled-in app modules
(RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www.
BBC module.yaml is published (clear streams); other module values stay local.
2026-10-06 00:25:35 +02:00

6.5 KiB

RTE module — filling in module.yaml

DASH + ModularDrm Widevine, captured from the phone. There is no public catalog, so every channel needs one phone capture to bootstrap.

Create apps/modules/rte/module.yaml. It is gitignored. Nothing below is shipped in the repo — you obtain it yourself from your own device.

Skeleton

package: <android package id>
license_url: "<Widevine ModularDrm endpoint, incl. its account query string>"
proxy_bin: bin/proxy-android-arm64
ca_hash: "6c3578b4"

score:
  hosts:
    - <manifest CDN host>
    - <ad-stitching host, if the app uses one>
  deny:
    - <EPG / schedules host fragment>

aliases:
  one: chanone
  two: chantwo

channels:
  chanone:
    label: "Channel One"
    chip_desc: ["^chanone$"]
    play_desc: ["play\\s*chanone"]
  chantwo:
    label: "Channel Two"
    chip_desc: ["^chantwo$"]
    play_desc: ["play\\s*chantwo"]

# Origin encoders behind the live channels.
origins:
  vc11: "https://<origin host>/live/<path>/vc11/vc11.isml/"
  vc12: "https://<origin host>/live/<path>/vc12/vc12.isml/"

kids:
  vc11: "<uuid-form KID>"
  vc12: "<uuid-form KID>"

Where each value comes from

package

Open the app on the phone, then:

adb shell dumpsys window | grep mCurrentFocus

The part before / is the package id.

license_url

Run a discover session and play a channel (capture.md):

./bin/drm proxy discover --install-ca --reinject
grep -iE 'ModularDrm|widevine|license' outputs/discover/*/appproxy.log

Take the full URL including its query string — the account reference in it is part of the endpoint. The module also accepts it per run:

./bin/drm capture --app rte --rte.license-url 'https://.../ModularDrm?...'

score.hosts / score.deny

hosts are the hosts that legitimately serve manifests; deny are hosts or URL fragments that must never be mistaken for one.

grep -oE '"host":"[^"]+"' outputs/discover/*/appproxy_traffic.jsonl \
  | sort | uniq -c | sort -rn | head -20

Put the origin and any ad-stitching host in hosts. Put EPG/schedule feeds and analytics in deny — this app's schedule feed otherwise scores as a manifest because its URL looks plausible.

Format-level scoring (.mpd, .m3u8, /manifest, .isml) is built in; you only supply the provider-specific parts.

channels.*.chip_desc and play_desc

These are the regexes that locate the channel chip on the Live tab and the large hero Play button. They match the node content-description, case-insensitively.

Navigate to the Live tab by hand and dump the tree:

adb shell uiautomator dump /sdcard/ui.xml
adb shell cat /sdcard/ui.xml | grep -oE 'content-desc="[^"]*"' | sort -u

You are looking for two things:

  • a small chip per channel — its description is usually the bare channel name, so anchor the regex (^chanone$) to avoid matching a longer label
  • a large "play " button that appears after the chip is tapped

The module taps the hero Play directly when it is already on screen, otherwise it taps the chip first and waits for Play to appear. A match is only accepted as the hero button if its area is at least hero_min_area (default 200000), which keeps small list-item play icons from winning.

origins and kids — the important pair

These let the manifest rewriter keep working after the ad-stitched manifest session expires. Both come from a capture.

The KID is in the capture session:

./bin/drm capture --app rte --channel chanone --auto-play --wvd data/device.wvd
jq -r '.kid' outputs/rte/latest/session.json

Convert it to dashed UUID form for the kids: map (8-4-4-4-12), e.g. df1633821dddfdd5bec9822c1ec0f052 becomes df163382-1ddd-fdd5-bec9-822c1ec0f052. Either form is accepted, but UUID form is easier to read.

The origin is the Smooth Streaming base URL. Find it in the discover traffic:

grep -oE 'https?://[^"]+\.isml[^"]*' outputs/discover/*/appproxy_traffic.jsonl \
  | sed -E 's#(\.isml).*#\1/#' | sort -u

The last path element before .isml is the encoder channel id, which becomes the map key. So https://host/live/tc-1/vc11/vc11.isml/ is keyed vc11.

Pair them up by capturing each channel in turn and noting which KID appeared:

for ch in chanone chantwo; do
  ./bin/drm capture --app rte --channel $ch --auto-play --wvd data/device.wvd
  echo "$ch -> $(jq -r .kid outputs/rte/latest/session.json)"
done

Then confirm the mapping resolves:

go -C apps/modules test -tags live ./rte/ -v

That test fetches each configured origin's manifest, checks the KID resolves back to its channel, and verifies the synthetic manifest lands on the live encoder's segment grid. If a KID does not resolve, kids: and origins: disagree.

ca_hash

The subject hash of the MITM CA, which is what the Magisk mount installs as <hash>.0. 6c3578b4 is the bundled CA; change it only if you generated your own.

adb shell 'ls /system/etc/security/cacerts/'

Optional tuning

All of these have working defaults — set them only if the app changes.

encoder_channel_re: '^(vc|channel)\d+$'   # which origin ids use the encoder timeline
origin_marker: .isml                      # path element identifying an origin BaseURL
hero_min_area: 200000                     # smallest node accepted as the hero Play

ui:
  launch_desc_contains: ["live tab", "header logo"]
  live_tab_desc_re: ["Live tab"]

timeouts:
  launch_wait_ui: 25s
  live_tab: 30s
  chip_wait: 20s
  play_wait: 15s
  playback: 45s

synthetic:
  video_timescale: 600
  audio_timescale: 48000
  video_name: video
  audio_name: audio_128k
  video_bandwidth: 6000000
  audio_bandwidth: 128000
  width: 1920
  height: 1080
  live_edge_offset_s: 12
  window_segments: 6

The synthetic block describes the origin encoder. Its defaults match a common Smooth-to-DASH setup; if your origin uses different stream names or bitrates, read them off the origin manifest:

curl -s 'https://<origin>/live/<path>/vc11/vc11.isml/Manifest' | head -40

StreamIndex Name="…" gives video_name / audio_name, and Bitrate="…" gives the bandwidths. Segment duration and phase are learned at runtime, not configured.

Verify

./bin/drm modules                         # channel count should match your file
./bin/drm capture --app rte --channel chanone --auto-play --wvd data/device.wvd
go -C apps/modules test -tags live ./rte/ -v

A good capture yields auth, pid, pssh and mpd, and a single KID:KEY whose KID matches the kids: entry for that channel's encoder.