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.
125 lines
4.4 KiB
Go
125 lines
4.4 KiB
Go
// Package tg4 is the TG4 app module: Brightcove HLS with a raw Widevine license.
|
|
// Its channels can be resolved from the public catalog (no phone), or captured
|
|
// through the phone MITM. Account id, policy key, video ids and the playback
|
|
// config URL all come from the local, untracked apps/modules/tg4/module.yaml, a
|
|
// flag, or the environment — never from this source.
|
|
package tg4
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"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.accountID, Name+".account-id", "", "override "+Name+" Brightcove account id")
|
|
fs.StringVar(&flags.policyKey, Name+".policy-key", "", "override "+Name+" Brightcove policy key")
|
|
fs.StringVar(&flags.playbackConfigURL, Name+".playback-config-url", "", "override "+Name+" playback config URL")
|
|
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 TG4 app plugin.
|
|
type App struct {
|
|
*provider.Base
|
|
cfg Config
|
|
}
|
|
|
|
// New constructs the plugin, loading values 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: Brightcove serves a raw octet-stream Widevine license.
|
|
func (a *App) KeyMode() string { return "raw" }
|
|
|
|
// CaptureHints: the MITM yields the license URL and HLS master; the PSSH is read
|
|
// from the master afterwards.
|
|
func (a *App) CaptureHints() capture.Hints {
|
|
return a.Hints("license_url", "mpd")
|
|
}
|
|
|
|
// MPDRewriter: HLS needs no manifest rewrite.
|
|
func (a *App) MPDRewriter() mpd.Rewriter { return mpd.Passthrough{} }
|
|
|
|
// Channels lists the configured live channels.
|
|
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 the app and waits for its home UI.
|
|
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()
|
|
return uiflow.WaitUI(c, a.CacheDir(), uiflow.Match{
|
|
DescContains: a.cfg.UI.LaunchDescContains,
|
|
ResourceContains: a.cfg.UI.LaunchResourceContains,
|
|
}, time.Duration(a.cfg.Timeouts.LaunchWaitUI))
|
|
}
|
|
|
|
// AutoPlay opens the Live page and taps this channel's card.
|
|
func (a *App) AutoPlay(c *adb.Client, channel string) error {
|
|
id, err := a.Base.Resolve(channel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ch := a.cfg.Channels[id]
|
|
fmt.Printf("[*] Auto-play %s…\n", a.Label(id))
|
|
|
|
if err := a.openLivePage(c); err != nil {
|
|
return err
|
|
}
|
|
if err := uiflow.TapCard(c, a.CacheDir(), ch.LiveCardIndex, time.Duration(a.cfg.Timeouts.Card), a.cfg.Cards); err != nil {
|
|
return err
|
|
}
|
|
time.Sleep(time.Duration(a.cfg.Timeouts.AfterCard))
|
|
|
|
// Soft-fail: this player often starts without a dumpsys audio session, so a
|
|
// UI signal is the real confirmation and capture should continue regardless.
|
|
return uiflow.WaitPlayback(c, a.CacheDir(), a.Package(), uiflow.PlaybackOpts{
|
|
Timeout: time.Duration(a.cfg.Timeouts.Playback),
|
|
SoftFail: true,
|
|
OrDescContains: a.cfg.UI.PlaybackDescContains,
|
|
OrResourceContains: a.cfg.UI.PlaybackResourceContains,
|
|
})
|
|
}
|
|
|
|
// StreamDefaults — multi-KID SAMPLE-AES HLS: take the best variant and give the
|
|
// muxer more bytes before probing, since early audio config is often incomplete.
|
|
func (a *App) VideoSelect() string { return "for=best" }
|
|
func (a *App) AudioSelect() string { return "for=best" }
|
|
func (a *App) RewriterName() string { return "none" }
|
|
func (a *App) LiveWaitSeconds() int { return 6 }
|
|
func (a *App) TSReadyBytes() int64 { return 2 << 20 }
|