Null-DRM-Official/apps/pkg/brightcove/brightcove.go
404errordeveloper 2fa8f2435f 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.
2026-10-06 00:25:35 +02:00

161 lines
4.6 KiB
Go

// Package brightcove talks to the Brightcove Playback API and reads Widevine
// material out of an HLS master. Brightcove is a platform, not a provider: this
// package holds no account, policy key, video id or channel knowledge.
package brightcove
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
)
// EdgeAPI is the Playback API base. Overridable for tests or a proxy.
var EdgeAPI = "https://edge.api.brightcove.com/playback/v1"
var (
// SESSION-KEY / KEY lines put URI and KEYFORMAT in either order.
reWidevinePSSH = regexp.MustCompile(`(?is)KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed".*?URI="data:text/plain;base64,([A-Za-z0-9+/=]+)"`)
reWidevinePSSH2 = regexp.MustCompile(`(?is)URI="data:text/plain;base64,([A-Za-z0-9+/=]+)".*?KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"`)
reKeyID = regexp.MustCompile(`(?i)keyId=([0-9a-f]{32})`)
)
// WidevineKeySystem is the key_systems map key for Widevine sources.
const WidevineKeySystem = "com.widevine.alpha"
// Video is the subset of a Playback API response we use.
type Video struct {
ID string `json:"id"`
Name string `json:"name"`
Sources []struct {
Type string `json:"type"`
Src string `json:"src"`
KeySystems map[string]struct {
LicenseURL string `json:"license_url"`
} `json:"key_systems"`
} `json:"sources"`
}
// PlaybackURL is the canonical (token-free) Playback API URL for a video.
func PlaybackURL(accountID, videoID string) string {
return fmt.Sprintf("%s/accounts/%s/videos/%s", EdgeAPI, accountID, videoID)
}
// FetchPlayback resolves a video through the Playback API. livePlaybackToken may
// be empty for assets that do not need one.
func FetchPlayback(accountID, videoID, livePlaybackToken, policyKey string) (Video, error) {
var v Video
if policyKey == "" {
return v, fmt.Errorf("policy key required")
}
if accountID == "" {
return v, fmt.Errorf("account id required")
}
u := PlaybackURL(accountID, videoID)
if livePlaybackToken != "" {
u += "?livePlaybackToken=" + livePlaybackToken
}
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return v, err
}
req.Header.Set("Accept", "application/json;pk="+policyKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return v, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return v, fmt.Errorf("brightcove playback HTTP %d: %s", resp.StatusCode, Trim(string(body), 200))
}
if err := json.Unmarshal(body, &v); err != nil {
return v, err
}
return v, nil
}
// PickHLSAndLicense chooses the best HLS source: prefer a DVR playlist that
// carries a Widevine license URL, then any Widevine source, then any DVR
// playlist, then any HLS at all.
func PickHLSAndLicense(v Video) (hls, license string) {
var dvr, dvrWV, anyWV, anyHLS string
var dvrLic, anyLic string
for _, s := range v.Sources {
if !strings.Contains(strings.ToLower(s.Type), "mpegurl") && !strings.Contains(strings.ToLower(s.Src), ".m3u8") {
continue
}
src := s.Src
lic := ""
if ks, ok := s.KeySystems[WidevineKeySystem]; ok {
lic = ks.LicenseURL
}
isDVR := strings.Contains(src, "playlist-hls-dvr.m3u8") || strings.Contains(src, "-dvr")
switch {
case isDVR && lic != "" && dvrWV == "":
dvrWV, dvrLic = src, lic
case isDVR && dvr == "":
dvr = src
case lic != "" && anyWV == "":
anyWV, anyLic = src, lic
case anyHLS == "":
anyHLS = src
}
}
switch {
case dvrWV != "":
return dvrWV, dvrLic
case anyWV != "":
return anyWV, anyLic
case dvr != "":
return dvr, ""
default:
return anyHLS, ""
}
}
// ExtractWidevinePSSH returns the base64 Widevine init data from an HLS master.
func ExtractWidevinePSSH(master string) string {
if m := reWidevinePSSH.FindStringSubmatch(master); len(m) == 2 {
return m[1]
}
if m := reWidevinePSSH2.FindStringSubmatch(master); len(m) == 2 {
return m[1]
}
return ""
}
// PrimaryKID returns the first keyId= seen in an HLS master, lowercased.
func PrimaryKID(master string) string {
if kids := reKeyID.FindAllStringSubmatch(master, -1); len(kids) > 0 {
return strings.ToLower(kids[0][1])
}
return ""
}
// Get fetches a URL as text (used for the HLS master).
func Get(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(b), nil
}
// Trim shortens a string for error messages.
func Trim(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}