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.
161 lines
4.6 KiB
Go
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] + "..."
|
|
}
|