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.
This commit is contained in:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

374
apps/pkg/adb/adb.go Normal file
View file

@ -0,0 +1,374 @@
package adb
import (
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)
// Client wraps the adb CLI. When Serial is set, every invocation uses -s SERIAL.
type Client struct {
Bin string
Serial string
}
func New() *Client {
bin := "adb"
if p, err := exec.LookPath("adb"); err == nil {
bin = p
}
return &Client{Bin: bin}
}
// WithSerial returns a copy that targets one device.
func (c *Client) WithSerial(serial string) *Client {
out := *c
out.Serial = strings.TrimSpace(serial)
return &out
}
func (c *Client) prefix(args []string) []string {
if c.Serial == "" {
return args
}
return append([]string{"-s", c.Serial}, args...)
}
func (c *Client) Run(args ...string) (string, string, error) {
cmd := exec.Command(c.Bin, c.prefix(args)...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return stdout.String(), stderr.String(), err
}
func (c *Client) Out(args ...string) string {
out, _, _ := c.Run(args...)
return strings.TrimSpace(out)
}
func (c *Client) EnsureDevice() error {
state := c.Out("get-state")
if state != "device" {
if c.Serial != "" {
return fmt.Errorf("adb device %s not ready (state=%q)", c.Serial, state)
}
return fmt.Errorf("adb device not ready (state=%q)", state)
}
return nil
}
// Device is one row from `adb devices -l`.
type Device struct {
Serial string
State string
Model string
Product string
USB string
}
// ListDevices returns every adb device row (any state). Uses a serial-less client.
func ListDevices(bin string) ([]Device, error) {
if bin == "" {
bin = New().Bin
}
cmd := exec.Command(bin, "devices", "-l")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("adb devices: %w (%s)", err, strings.TrimSpace(stderr.String()))
}
var out []Device
for _, line := range strings.Split(stdout.String(), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "List of devices") {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
d := Device{Serial: fields[0], State: fields[1]}
for _, f := range fields[2:] {
if strings.HasPrefix(f, "model:") {
d.Model = strings.TrimPrefix(f, "model:")
}
if strings.HasPrefix(f, "product:") {
d.Product = strings.TrimPrefix(f, "product:")
}
if strings.HasPrefix(f, "usb:") {
d.USB = strings.TrimPrefix(f, "usb:")
}
}
out = append(out, d)
}
return out, nil
}
// PackageInstalled is true when `pm path <pkg>` returns a path. Never installs.
func (c *Client) PackageInstalled(pkg string) bool {
pkg = strings.TrimSpace(pkg)
if pkg == "" {
return false
}
out := c.Out("shell", "pm", "path", pkg)
return strings.Contains(out, "package:")
}
// ForceStop stops the package so the phone is free for the next job.
func (c *Client) ForceStop(pkg string) {
if pkg == "" {
return
}
_, _ = c.Shell("am", "force-stop", pkg)
}
// EnsureAwake wakes the screen. Call this BEFORE launching the target app.
// It must not press HOME or swipe after the app is open — dumpsys always
// contains "StatusBar", and a HOME key looks like "the app just exits".
func (c *Client) EnsureAwake() {
_, _ = c.Shell("input", "keyevent", "KEYCODE_WAKEUP")
time.Sleep(250 * time.Millisecond)
_, _ = c.Shell("wm", "dismiss-keyguard")
if currentFocusIsLockOrShade(c.Out("shell", "dumpsys", "window")) {
_, _ = c.Shell("input", "swipe", "540", "2000", "540", "600", "300")
time.Sleep(400 * time.Millisecond)
c.DismissShadeIfFocused()
}
}
func currentFocusIsLockOrShade(dumpsys string) bool {
for _, line := range strings.Split(dumpsys, "\n") {
line = strings.TrimSpace(line)
if !strings.Contains(line, "mCurrentFocus=") {
continue
}
low := strings.ToLower(line)
return strings.Contains(low, "notificationshade") ||
strings.Contains(low, "keyguard") ||
strings.Contains(low, "lockscreen")
}
return false
}
// DismissShadeIfFocused presses BACK only when the notification shade has focus.
func (c *Client) DismissShadeIfFocused() {
if currentFocusIsLockOrShade(c.Out("shell", "dumpsys", "window")) {
_, _ = c.Shell("input", "keyevent", "KEYCODE_BACK")
time.Sleep(200 * time.Millisecond)
}
}
func (c *Client) Shell(args ...string) (string, error) {
all := append([]string{"shell"}, args...)
out, errOut, err := c.Run(all...)
if err != nil {
if strings.TrimSpace(errOut) != "" {
return out, fmt.Errorf("%w: %s", err, strings.TrimSpace(errOut))
}
return out, err
}
return out, nil
}
func (c *Client) Push(local, remote string) error {
_, errOut, err := c.Run("push", local, remote)
if err != nil {
return fmt.Errorf("adb push: %w (%s)", err, strings.TrimSpace(errOut))
}
return nil
}
func (c *Client) Pull(remote, local string) error {
_, errOut, err := c.Run("pull", remote, local)
if err != nil {
return fmt.Errorf("adb pull: %w (%s)", err, strings.TrimSpace(errOut))
}
return nil
}
func (c *Client) Tap(x, y int) error {
_, err := c.Shell("input", "tap", strconv.Itoa(x), strconv.Itoa(y))
return err
}
// Swipe performs an input swipe over durationMs milliseconds.
func (c *Client) Swipe(x1, y1, x2, y2, durationMs int) error {
if durationMs <= 0 {
durationMs = 300
}
_, err := c.Shell("input", "swipe",
strconv.Itoa(x1), strconv.Itoa(y1),
strconv.Itoa(x2), strconv.Itoa(y2),
strconv.Itoa(durationMs))
return err
}
// Node is one uiautomator element.
type Node struct {
Text string
Desc string
Class string
ResourceID string
Clickable bool
X1, Y1 int
X2, Y2 int
}
func (n Node) CX() int { return (n.X1 + n.X2) / 2 }
func (n Node) CY() int { return (n.Y1 + n.Y2) / 2 }
func (n Node) Area() int {
return (n.X2 - n.X1) * (n.Y2 - n.Y1)
}
var (
reNode = regexp.MustCompile(`<node [^>]+>`)
reAttr = func(k string) *regexp.Regexp { return regexp.MustCompile(k + `="([^"]*)"`) }
reBounds = regexp.MustCompile(`\[(\d+),(\d+)\]\[(\d+),(\d+)\]`)
)
func parseNodes(xml string) []Node {
var out []Node
for _, m := range reNode.FindAllString(xml, -1) {
attr := func(k string) string {
mm := reAttr(k).FindStringSubmatch(m)
if len(mm) < 2 {
return ""
}
return mm[1]
}
b := attr("bounds")
bm := reBounds.FindStringSubmatch(b)
if len(bm) != 5 {
continue
}
x1, _ := strconv.Atoi(bm[1])
y1, _ := strconv.Atoi(bm[2])
x2, _ := strconv.Atoi(bm[3])
y2, _ := strconv.Atoi(bm[4])
out = append(out, Node{
Text: attr("text"),
Desc: attr("content-desc"),
Class: attr("class"),
ResourceID: attr("resource-id"),
Clickable: attr("clickable") == "true",
X1: x1, Y1: y1, X2: x2, Y2: y2,
})
}
return out
}
// DumpUI pulls a uiautomator hierarchy.
func (c *Client) DumpUI(cacheDir string) ([]Node, error) {
remote := "/sdcard/streamd_uidump.xml"
if _, err := c.Shell("uiautomator", "dump", remote); err != nil {
// dump often returns exit 0 with message; ignore soft failures
}
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return nil, err
}
local := cacheDir + string(os.PathSeparator) + "uidump.xml"
if err := c.Pull(remote, local); err != nil {
return nil, err
}
raw, err := os.ReadFile(local)
if err != nil {
return nil, err
}
return parseNodes(string(raw)), nil
}
// FindLargest returns the largest node matching any text/desc regex.
func FindLargest(nodes []Node, textPats, descPats []*regexp.Regexp) *Node {
var hits []Node
for _, n := range nodes {
ok := false
if n.Text != "" {
for _, p := range textPats {
if p.MatchString(n.Text) {
ok = true
break
}
}
}
if n.Desc != "" {
for _, p := range descPats {
if p.MatchString(n.Desc) {
ok = true
break
}
}
}
if ok {
hits = append(hits, n)
}
}
if len(hits) == 0 {
return nil
}
best := hits[0]
for _, h := range hits[1:] {
if h.Area() > best.Area() {
best = h
}
}
return &best
}
// FindSmallest prefers tiny chips over hero buttons.
func FindSmallest(nodes []Node, descPats []*regexp.Regexp) *Node {
var hits []Node
for _, n := range nodes {
if n.Desc == "" {
continue
}
for _, p := range descPats {
if p.MatchString(n.Desc) {
hits = append(hits, n)
break
}
}
}
if len(hits) == 0 {
return nil
}
best := hits[0]
for _, h := range hits[1:] {
if h.Area() < best.Area() {
best = h
}
}
return &best
}
// WaitFor polls DumpUI until a match appears or timeout.
func (c *Client) WaitFor(cacheDir string, textPats, descPats []*regexp.Regexp, timeout time.Duration) (*Node, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
nodes, err := c.DumpUI(cacheDir)
if err == nil {
if n := FindLargest(nodes, textPats, descPats); n != nil {
return n, nil
}
}
time.Sleep(800 * time.Millisecond)
}
return nil, fmt.Errorf("ui node not found within %s", timeout)
}
// PlaybackActive is true when the package media session is PLAYING.
func (c *Client) PlaybackActive(pkg string) bool {
out := c.Out("shell", "dumpsys", "media_session")
if !strings.Contains(out, pkg) {
return false
}
return strings.Contains(out, "state=PLAYING") || strings.Contains(out, "PLAYING(")
}