// 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 }