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.
151 lines
4.2 KiB
Go
151 lines
4.2 KiB
Go
package tg4
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
appreg "drmdecryption/app"
|
|
"drmdecryption/brightcove"
|
|
"drmdecryption/session"
|
|
)
|
|
|
|
// playbackConfig is the app's remote config blob. It is decoded generically: the
|
|
// field names a channel uses come from module.yaml (token_field / stream_id_field),
|
|
// so a renamed or added field needs no code change.
|
|
type playbackConfig map[string]any
|
|
|
|
func fetchPlaybackConfig(url string) (playbackConfig, error) {
|
|
if url == "" {
|
|
return nil, fmt.Errorf("playback_config_url required (set it in module.yaml or --%s.playback-config-url)", Name)
|
|
}
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("playback config HTTP %d", resp.StatusCode)
|
|
}
|
|
var pc playbackConfig
|
|
if err := json.NewDecoder(resp.Body).Decode(&pc); err != nil {
|
|
return nil, err
|
|
}
|
|
return pc, nil
|
|
}
|
|
|
|
// str reads a string field, tolerating numbers.
|
|
func (pc playbackConfig) str(field string) string {
|
|
if field == "" {
|
|
return ""
|
|
}
|
|
switch v := pc[field].(type) {
|
|
case string:
|
|
return v
|
|
case float64:
|
|
return fmt.Sprintf("%.0f", v)
|
|
case json.Number:
|
|
return v.String()
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Flags reports the boolean switches in the playback config, for `--list`.
|
|
func (pc playbackConfig) Flags() map[string]bool {
|
|
out := map[string]bool{}
|
|
for k, v := range pc {
|
|
if b, ok := v.(bool); ok {
|
|
out[k] = b
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// resolveChannel returns the channel's label, Brightcove video id and playback
|
|
// token, consulting the remote playback config for anything not pinned locally.
|
|
func (a *App) resolveChannel(pc playbackConfig, id string) (label, videoID, token string, err error) {
|
|
ch, ok := a.cfg.Channels[id]
|
|
if !ok {
|
|
return "", "", "", fmt.Errorf("unknown channel %q", id)
|
|
}
|
|
label = a.Label(id)
|
|
videoID = ch.VideoID
|
|
if videoID == "" {
|
|
videoID = pc.str(ch.StreamIDField)
|
|
}
|
|
token = pc.str(ch.TokenField)
|
|
|
|
if videoID == "" {
|
|
if ch.PhoneOnly {
|
|
return "", "", "", fmt.Errorf("channel %q has no catalog entry — capture it from the phone: drm capture --app %s --channel %s", id, Name, id)
|
|
}
|
|
return "", "", "", fmt.Errorf("no video id for channel %q (set video_id or stream_id_field in %s)", id, a.ConfigPath())
|
|
}
|
|
if token == "" && ch.TokenField != "" {
|
|
return label, videoID, "", fmt.Errorf("no playback token for channel %q (field %s)", id, ch.TokenField)
|
|
}
|
|
return label, videoID, token, nil
|
|
}
|
|
|
|
// Resolve implements app.Catalog: build streamd-ready credentials for a channel
|
|
// without touching a phone.
|
|
func (a *App) Resolve(channel string) (session.Stream, error) {
|
|
id, err := a.Base.Resolve(channel)
|
|
if err != nil {
|
|
return session.Stream{}, err
|
|
}
|
|
pc, err := fetchPlaybackConfig(a.cfg.PlaybackConfigURL)
|
|
if err != nil {
|
|
return session.Stream{}, err
|
|
}
|
|
label, videoID, token, err := a.resolveChannel(pc, id)
|
|
if err != nil {
|
|
return session.Stream{}, err
|
|
}
|
|
vid, err := brightcove.FetchPlayback(a.cfg.AccountID, videoID, token, a.cfg.PolicyKey)
|
|
if err != nil {
|
|
return session.Stream{}, err
|
|
}
|
|
hls, license := brightcove.PickHLSAndLicense(vid)
|
|
if hls == "" {
|
|
return session.Stream{}, fmt.Errorf("no HLS source in playback response for %s", videoID)
|
|
}
|
|
title := label
|
|
if vid.Name != "" {
|
|
title = vid.Name
|
|
}
|
|
return session.Stream{
|
|
Name: a.cfg.StreamNamePrefix + "-" + strings.ToLower(id),
|
|
Title: title,
|
|
App: Name,
|
|
Channel: strings.ToLower(id),
|
|
MPD: hls,
|
|
HeadersJSON: "{}",
|
|
Rewriter: "none",
|
|
LicenseURL: license,
|
|
AccountID: a.cfg.AccountID,
|
|
VideoID: videoID,
|
|
PlaybackURL: brightcove.PlaybackURL(a.cfg.AccountID, videoID),
|
|
ManifestType: "hls",
|
|
}, nil
|
|
}
|
|
|
|
// ListChannels resolves each configured channel's video id for `catalog --list`.
|
|
func (a *App) ListChannels() ([]appreg.CatalogRow, map[string]bool, error) {
|
|
pc, err := fetchPlaybackConfig(a.cfg.PlaybackConfigURL)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
rows := make([]appreg.CatalogRow, 0, len(a.cfg.Channels))
|
|
for _, id := range a.ChannelIDs() {
|
|
ch := a.cfg.Channels[id]
|
|
vid := ch.VideoID
|
|
if vid == "" {
|
|
vid = pc.str(ch.StreamIDField)
|
|
}
|
|
rows = append(rows, appreg.CatalogRow{ID: id, CatalogID: vid, Label: a.Label(id)})
|
|
}
|
|
return rows, pc.Flags(), nil
|
|
}
|