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:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
129
apps/modules/bbc/bbc.go
Normal file
129
apps/modules/bbc/bbc.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Package bbc is the BBC iPlayer app module: clear DASH/HLS captured through
|
||||
// transparent MITM (UK VPN stays on). Only the manifest URL is required — no
|
||||
// Widevine keys for the mobile-phone-main mediaset streams we capture.
|
||||
package bbc
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"drmdecryption/adb"
|
||||
appreg "drmdecryption/app"
|
||||
"drmdecryption/capture"
|
||||
"drmdecryption/modules/provider"
|
||||
"drmdecryption/mpd"
|
||||
"drmdecryption/uiflow"
|
||||
)
|
||||
|
||||
func init() {
|
||||
appreg.Register(Name, New)
|
||||
appreg.RegisterFlags(bindFlags)
|
||||
}
|
||||
|
||||
func bindFlags(fs *flag.FlagSet) {
|
||||
fs.StringVar(&flags.packageName, Name+".package", "", "override "+Name+" android package id")
|
||||
fs.StringVar(&flags.caHash, Name+".ca-hash", "", "override "+Name+" MITM CA subject hash")
|
||||
fs.StringVar(&flags.proxyBin, Name+".proxy-bin", "", "override "+Name+" on-device MITM binary")
|
||||
}
|
||||
|
||||
// App is the BBC iPlayer plugin.
|
||||
type App struct {
|
||||
*provider.Base
|
||||
cfg Config
|
||||
}
|
||||
|
||||
// New constructs the plugin from module.yaml + flags + env.
|
||||
func New() (appreg.App, error) {
|
||||
cfg, res, err := loadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labels := make(map[string]string, len(cfg.Channels))
|
||||
for id, ch := range cfg.Channels {
|
||||
labels[id] = ch.Label
|
||||
}
|
||||
return &App{Base: provider.New(Name, cfg.Common, labels, res), cfg: cfg}, nil
|
||||
}
|
||||
|
||||
// KeyMode none: mobile streams are clear (CDN signed URLs); no wvkey.
|
||||
func (a *App) KeyMode() string { return "none" }
|
||||
|
||||
// CaptureHints: only the MPD/HLS master from mediaselector is required.
|
||||
func (a *App) CaptureHints() capture.Hints {
|
||||
return a.Hints("mpd")
|
||||
}
|
||||
|
||||
// UseTransparentMITM: NordVPN UK conflicts with Wi‑Fi http_proxy.
|
||||
func (a *App) UseTransparentMITM() bool { return true }
|
||||
|
||||
// MPDRewriter: passthrough — no rewrite needed for clear BBC DASH.
|
||||
func (a *App) MPDRewriter() mpd.Rewriter { return mpd.Passthrough{} }
|
||||
|
||||
// Channels lists configured live targets.
|
||||
func (a *App) Channels() []appreg.Channel {
|
||||
out := make([]appreg.Channel, 0, len(a.cfg.Channels))
|
||||
for _, id := range a.ChannelIDs() {
|
||||
out = append(out, appreg.Channel{ID: id, Label: a.Label(id)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Launch cold-starts iPlayer.
|
||||
func (a *App) Launch(c *adb.Client) error {
|
||||
c.EnsureAwake()
|
||||
fmt.Printf("[*] Launching %s (%s)…\n", Name, a.Package())
|
||||
c.ForceStop(a.Package())
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
if err := uiflow.MonkeyLaunch(c, a.Package()); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Duration(a.cfg.Timeouts.LaunchSettle))
|
||||
c.DismissShadeIfFocused()
|
||||
if err := uiflow.WaitUI(c, a.CacheDir(), uiflow.Match{
|
||||
DescContains: a.cfg.UI.LaunchDescContains,
|
||||
ResourceContains: a.cfg.UI.LaunchResourceContains,
|
||||
}, time.Duration(a.cfg.Timeouts.LaunchWaitUI)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[!] launch UI wait: %v — continue and play manually\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoPlay: best-effort; iPlayer UI varies — operator can play by hand.
|
||||
func (a *App) AutoPlay(c *adb.Client, channel string) error {
|
||||
id, err := a.Resolve(channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ch := a.cfg.Channels[id]
|
||||
fmt.Printf("[*] Auto-play %s — open Live and start playback on the phone…\n", a.Label(id))
|
||||
if ch.VPID != "" {
|
||||
fmt.Printf(" expected mediaselector vpid ≈ %s\n", ch.VPID)
|
||||
}
|
||||
c.DismissShadeIfFocused()
|
||||
if len(ch.PlayDesc) == 0 && len(ch.ChipDesc) == 0 {
|
||||
return fmt.Errorf("no UI selectors for %s — play manually on the phone", id)
|
||||
}
|
||||
if len(ch.ChipDesc) > 0 {
|
||||
if err := uiflow.TapUI(c, a.CacheDir(), uiflow.Match{DescRE: ch.ChipDesc}, 20*time.Second); err != nil {
|
||||
return fmt.Errorf("channel chip: %w", err)
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
if len(ch.PlayDesc) > 0 {
|
||||
if err := uiflow.TapUI(c, a.CacheDir(), uiflow.Match{DescRE: ch.PlayDesc}, 15*time.Second); err != nil {
|
||||
return fmt.Errorf("play: %w", err)
|
||||
}
|
||||
}
|
||||
return uiflow.WaitPlayback(c, a.CacheDir(), a.Package(), uiflow.PlaybackOpts{
|
||||
Timeout: time.Duration(a.cfg.Timeouts.Playback),
|
||||
})
|
||||
}
|
||||
|
||||
// StreamDefaults for clear live DASH.
|
||||
func (a *App) VideoSelect() string { return "res=1280x720:for=best" }
|
||||
func (a *App) AudioSelect() string { return "lang=en:for=best" }
|
||||
func (a *App) RewriterName() string { return "none" }
|
||||
func (a *App) LiveWaitSeconds() int { return 2 }
|
||||
func (a *App) TSReadyBytes() int64 { return 256 << 10 }
|
||||
80
apps/modules/bbc/bbc_test.go
Normal file
80
apps/modules/bbc/bbc_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package bbc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
appreg "drmdecryption/app"
|
||||
"drmdecryption/capture"
|
||||
"drmdecryption/modcfg"
|
||||
"drmdecryption/modules/provider"
|
||||
"drmdecryption/mpd"
|
||||
)
|
||||
|
||||
var (
|
||||
_ appreg.App = (*App)(nil)
|
||||
_ appreg.StreamDefaults = (*App)(nil)
|
||||
_ appreg.TransparentMITM = (*App)(nil)
|
||||
)
|
||||
|
||||
func testApp(t *testing.T) *App {
|
||||
t.Helper()
|
||||
cfg := Config{
|
||||
Channels: map[string]Channel{
|
||||
"bbcone": {Label: "BBC One", VPID: "bbc_one_london"},
|
||||
"bbctwo": {Label: "BBC Two", VPID: "bbc_two_england"},
|
||||
},
|
||||
Common: provider.Common{
|
||||
Package: "bbc.iplayer.android",
|
||||
Aliases: map[string]string{"one": "bbcone", "bbc1": "bbcone"},
|
||||
Score: capture.ScoreCfg{
|
||||
Hosts: []string{"open.live.bbc.co.uk", "vs-cmaf-push-uk"},
|
||||
},
|
||||
},
|
||||
}.withDefaults()
|
||||
labels := map[string]string{}
|
||||
for id, ch := range cfg.Channels {
|
||||
labels[id] = ch.Label
|
||||
}
|
||||
return &App{Base: provider.New(Name, cfg.Common, labels, modcfg.Result{Path: "test"}), cfg: cfg}
|
||||
}
|
||||
|
||||
func TestNoHardcodedSecrets(t *testing.T) {
|
||||
cfg := Config{}.withDefaults()
|
||||
if cfg.Package != "" || cfg.LicenseURL != "" {
|
||||
t.Fatalf("empty config must not invent package/license: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyModeNoneAndMPDOnly(t *testing.T) {
|
||||
a := testApp(t)
|
||||
if a.KeyMode() != "none" {
|
||||
t.Fatalf("KeyMode = %q, want none", a.KeyMode())
|
||||
}
|
||||
h := a.CaptureHints()
|
||||
if len(h.Require) != 1 || h.Require[0] != "mpd" {
|
||||
t.Fatalf("Require = %v, want [mpd]", h.Require)
|
||||
}
|
||||
if !a.UseTransparentMITM() {
|
||||
t.Fatal("BBC must use transparent MITM (UK VPN)")
|
||||
}
|
||||
if _, ok := a.MPDRewriter().(mpd.Passthrough); !ok {
|
||||
t.Fatal("expected Passthrough rewriter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliases(t *testing.T) {
|
||||
a := testApp(t)
|
||||
id, err := a.Resolve("one")
|
||||
if err != nil || id != "bbcone" {
|
||||
t.Fatalf("Resolve(one) = %q %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureHintsScoreHosts(t *testing.T) {
|
||||
a := testApp(t)
|
||||
h := a.CaptureHints()
|
||||
u := "https://vs-cmaf-push-uk.live.fastly.md.bbci.co.uk/x/mobile.mpd"
|
||||
if h.Score.Score(u) <= 0 {
|
||||
t.Fatalf("expected positive score for %s (score=%d)", u, h.Score.Score(u))
|
||||
}
|
||||
}
|
||||
74
apps/modules/bbc/config.go
Normal file
74
apps/modules/bbc/config.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package bbc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"drmdecryption/modcfg"
|
||||
"drmdecryption/modules/provider"
|
||||
)
|
||||
|
||||
// Name is the module id (--app bbc).
|
||||
const Name = "bbc"
|
||||
|
||||
// Channel is one live (or catch-up) target. Phone UI selectors are optional —
|
||||
// capture works with manual play; auto-play uses them when present.
|
||||
type Channel struct {
|
||||
Label string `yaml:"label"`
|
||||
// VPID is the mediaselector version/service id (e.g. bbc_one_london).
|
||||
VPID string `yaml:"vpid"`
|
||||
// ChipDesc / PlayDesc are optional UI matchers for --auto-play.
|
||||
ChipDesc []string `yaml:"chip_desc"`
|
||||
PlayDesc []string `yaml:"play_desc"`
|
||||
}
|
||||
|
||||
// Config is apps/modules/bbc/module.yaml.
|
||||
type Config struct {
|
||||
provider.Common `yaml:",inline"`
|
||||
|
||||
Channels map[string]Channel `yaml:"channels"`
|
||||
|
||||
Timeouts Timeouts `yaml:"timeouts"`
|
||||
UI UI `yaml:"ui"`
|
||||
}
|
||||
|
||||
// Timeouts for launch / optional autoplay.
|
||||
type Timeouts struct {
|
||||
LaunchWaitUI provider.Duration `yaml:"launch_wait_ui"`
|
||||
LaunchSettle provider.Duration `yaml:"launch_settle"`
|
||||
Playback provider.Duration `yaml:"playback"`
|
||||
}
|
||||
|
||||
// UI markers for launch readiness (localized; keep loose).
|
||||
type UI struct {
|
||||
LaunchDescContains []string `yaml:"launch_desc_contains"`
|
||||
LaunchResourceContains []string `yaml:"launch_resource_contains"`
|
||||
}
|
||||
|
||||
func (c Config) withDefaults() Config {
|
||||
t := &c.Timeouts
|
||||
t.LaunchWaitUI = provider.Duration(t.LaunchWaitUI.D(30 * time.Second))
|
||||
t.LaunchSettle = provider.Duration(t.LaunchSettle.D(4 * time.Second))
|
||||
t.Playback = provider.Duration(t.Playback.D(60 * time.Second))
|
||||
if len(c.UI.LaunchDescContains) == 0 {
|
||||
c.UI.LaunchDescContains = []string{"iPlayer", "Home", "Live"}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
var flags struct {
|
||||
packageName string
|
||||
caHash string
|
||||
proxyBin string
|
||||
}
|
||||
|
||||
func loadConfig() (Config, modcfg.Result, error) {
|
||||
var cfg Config
|
||||
res, err := modcfg.Load(Name, &cfg)
|
||||
if err != nil {
|
||||
return cfg, res, err
|
||||
}
|
||||
cfg.Package = modcfg.Override(Name, "package", flags.packageName, cfg.Package)
|
||||
cfg.CAHash = modcfg.Override(Name, "ca_hash", flags.caHash, cfg.CAHash)
|
||||
cfg.ProxyBin = modcfg.Override(Name, "proxy_bin", flags.proxyBin, cfg.ProxyBin)
|
||||
return cfg.withDefaults(), res, nil
|
||||
}
|
||||
51
apps/modules/bbc/module.yaml
Normal file
51
apps/modules/bbc/module.yaml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# BBC iPlayer — published values. Package id and channel map only;
|
||||
# no Widevine license URL (mobile-phone-main streams are clear).
|
||||
|
||||
package: bbc.iplayer.android
|
||||
proxy_bin: bin/proxy-android-arm64
|
||||
ca_hash: "6c3578b4"
|
||||
|
||||
# Manifest-URL scoring for the on-device MITM / waiter.
|
||||
score:
|
||||
hosts:
|
||||
- open.live.bbc.co.uk
|
||||
- vs-cmaf-push-uk
|
||||
- vod-dash-uk
|
||||
- akamaized.net
|
||||
- bbci.co.uk
|
||||
- bidi.net.uk
|
||||
- bidi.bbc.co.uk
|
||||
deny:
|
||||
- telemetry.api.bbci
|
||||
- bag.api.bbc
|
||||
- ibl.api.bbci
|
||||
- thumbnail
|
||||
|
||||
aliases:
|
||||
one: bbcone
|
||||
bbc1: bbcone
|
||||
london: bbcone
|
||||
two: bbctwo
|
||||
bbc2: bbctwo
|
||||
news: bbcnews
|
||||
four: bbcfour
|
||||
|
||||
channels:
|
||||
bbcone:
|
||||
label: BBC One
|
||||
vpid: bbc_one_london
|
||||
bbctwo:
|
||||
label: BBC Two
|
||||
vpid: bbc_two_england
|
||||
bbcnews:
|
||||
label: BBC News
|
||||
vpid: bbc_news_channel
|
||||
bbcfour:
|
||||
label: BBC Four
|
||||
vpid: bbc_four
|
||||
|
||||
# timeouts:
|
||||
# launch_wait_ui: 30s
|
||||
# playback: 60s
|
||||
# ui:
|
||||
# launch_desc_contains: ["iPlayer", "Home", "Live"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue