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
151
apps/modules/tg4/catalog.go
Normal file
151
apps/modules/tg4/catalog.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue