Skip to content

Commit

Permalink
feat: support for Widevine (+PlayReady?) via CPIX.
Browse files Browse the repository at this point in the history
Added new configuration file for DRM.
Updated urlgen page to support DRM options.
Unified ECCP and DRM into a common URL configuration.
Old /eccp_cbcs and /eccp_cenc are still working.
Also checks for pre-encrypted content.
Support different keys for video and audio if available.
  • Loading branch information
tobbee committed Dec 3, 2024
1 parent 14c16ae commit 05dd3a7
Show file tree
Hide file tree
Showing 32 changed files with 974 additions and 254 deletions.
11 changes: 11 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"cenc",
"certmagic",
"certpath",
"Cfgs",
"cfhd",
"chunkdur",
"chunkparser",
Expand All @@ -37,6 +38,7 @@
"commandline",
"configprocessor",
"confmap",
"CPIX",
"curr",
"danielgtaylor",
"dashif",
Expand All @@ -49,6 +51,8 @@
"encv",
"etree",
"Eyevinn",
"EZDRM",
"fairplay",
"genurl",
"golangci",
"gzipped",
Expand Down Expand Up @@ -104,6 +108,7 @@
"Payl",
"peroff",
"pflag",
"playready",
"playurl",
"posflag",
"pprof",
Expand All @@ -119,11 +124,15 @@
"reqlimitlog",
"resegmentation",
"RLSAKT",
"saio",
"Schm",
"SCTE",
"segmtimeline",
"segtimeline",
"segtimelineloss",
"Sidx",
"Sidxs",
"Sinf",
"Sntp",
"startrel",
"statuscode",
Expand All @@ -138,6 +147,7 @@
"Sttp",
"styp",
"subsstppreg",
"tenc",
"testdata",
"testpic",
"testvalues",
Expand Down Expand Up @@ -173,6 +183,7 @@
"vtte",
"waitgroup",
"WEBVTT",
"widevine",
"writerepdata",
"WVTT",
"xlink",
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet
### Added

- On-the-fly encryption with keys from commercial DRM (Widevine and PlayReady) via CPIX document
- DRM configuration and URL generation on `urlgen` page
- Unified ECCP and other DRMs using the new URL parameter `/drm_X`

### Fixed

- CLI parameter -h for livesim2

## [1.5.2] - 2024-11-05

Expand Down
125 changes: 72 additions & 53 deletions cmd/livesim2/app/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (am *assetMgr) loadAsset(logger *slog.Logger, mpdPath string) error {
}

func (am *assetMgr) loadRep(logger *slog.Logger, assetPath string, as *m.AdaptationSetType, rep *m.RepresentationType) (*RepData, error) {
logger = logger.With("rep", rep.Id, "assetPath", assetPath)
logger = logger.With("rep", rep.Id)
rp := RepData{ID: rep.Id,
ContentType: string(as.ContentType),
Codecs: as.Codecs,
Expand All @@ -182,7 +182,7 @@ func (am *assetMgr) loadRep(logger *slog.Logger, assetPath string, as *m.Adaptat
if !am.writeRepData {
ok, err := rp.loadFromJSON(logger, am.vodFS, am.repDataDir, assetPath)
if ok {
logger.Debug("Loaded representation data from JSON", "rep", rp.ID, "asset", assetPath)
logger.Debug("Loaded representation data from JSON")
return &rp, err
}
}
Expand Down Expand Up @@ -301,7 +301,6 @@ segLoop:

// loadFromJSON reads the representation data from a gzipped or plain JSON file.
func (rp *RepData) loadFromJSON(logger *slog.Logger, vodFS fs.FS, repDataDir, assetPath string) (bool, error) {
logger = logger.With("rep", rp.ID, "assetPath", assetPath)
if repDataDir == "" {
return false, nil
}
Expand Down Expand Up @@ -677,15 +676,17 @@ type RepData struct {
}

type repEncData struct {
keyID id16 // Should be common within one AdaptationSet, but for now common for one asset
key id16 // Should be common within one AdaptationSet, but for now common for one asset
iv []byte // Can be random, but we use a constant default value at start
cbcsPD *mp4.InitProtectData
cencPD *mp4.InitProtectData
cbcsInitSeg *mp4.InitSegment
cencInitSeg *mp4.InitSegment
cbcsInitBytes []byte
cencInitBytes []byte
keyID id16 // Should be common within one AdaptationSet, but for now common for one asset
key id16 // Should be common within one AdaptationSet, but for now common for one asset
iv []byte // Can be random, but we use a constant default value at start
initEnc map[string]initEncData
}

type initEncData struct {
scheme string
pd *mp4.InitProtectData
init *mp4.InitSegment
initRaw []byte
}

var defaultIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
Expand Down Expand Up @@ -750,11 +751,11 @@ func prepareForEncryption(codec string) bool {
}

func (r *RepData) readInit(logger *slog.Logger, vodFS fs.FS, assetPath string) error {
data, err := fs.ReadFile(vodFS, path.Join(assetPath, r.InitURI))
rawInit, err := fs.ReadFile(vodFS, path.Join(assetPath, r.InitURI))
if err != nil {
return fmt.Errorf("read initURI %q: %w", r.InitURI, err)
}
r.initSeg, err = getInitSeg(data)
r.initSeg, err = getInitSeg(rawInit)
if err != nil {
return fmt.Errorf("decode init: %w", err)
}
Expand All @@ -765,7 +766,7 @@ func (r *RepData) readInit(logger *slog.Logger, vodFS fs.FS, assetPath string) e

if prepareForEncryption(r.Codecs) {
assetName := path.Base(assetPath)
err = r.addEncryption(logger, assetName, data)
err = r.addEncryption(logger, assetName)
if err != nil {
return fmt.Errorf("addEncryption: %w", err)
}
Expand All @@ -782,68 +783,86 @@ func (r *RepData) readInit(logger *slog.Logger, vodFS fs.FS, assetPath string) e
return nil
}

func (r *RepData) addEncryption(logger *slog.Logger, assetName string, data []byte) error {
// Set up the encryption data for this representation given asset
ed := repEncData{}
ed.keyID = kidFromString(assetName)
ed.key = kidToKey(ed.keyID)
ed.iv = defaultIV

// Generate cbcs data, but exit if already encrypted
initSeg, err := getInitSeg(data)
func checkPreEncrypted(logger *slog.Logger, rawInit []byte) (bool, error) {
initSeg, err := getInitSeg(rawInit)
if err != nil {
return fmt.Errorf("decode init: %w", err)
return false, fmt.Errorf("decode init: %w", err)
}
stsd := initSeg.Moov.Trak.Mdia.Minf.Stbl.Stsd
for _, c := range stsd.Children {
switch box := c.(type) {
case *mp4.VisualSampleEntryBox:
if box.Type() == "encv" && box.Sinf != nil && box.Sinf.Schm != nil {
logger.Info("Video pre-encrypted", "repID", r.ID, "scheme", box.Sinf.Schm.SchemeType, "init", r.InitURI)
r.PreEncrypted = true
return nil
logger.Info("Video pre-encrypted", "scheme", box.Sinf.Schm.SchemeType)
return true, nil
}
case *mp4.AudioSampleEntryBox:
if box.Type() == "enca" && box.Sinf != nil && box.Sinf.Schm != nil {
logger.Info("Video pre-encrypted", "repID", r.ID, "scheme", box.Sinf.Schm.SchemeType, "init", r.InitURI)
r.PreEncrypted = true
return nil
logger.Info("Audio pre-encrypted", "scheme", box.Sinf.Schm.SchemeType)
return true, nil
}
}
}
kid, err := mp4.NewUUIDFromHex(hex.EncodeToString(ed.keyID[:]))
return false, nil
}

// genEncInit generates an init segment adapted for encrypted content
func genEncInit(rawInit []byte, kid id16, iv []byte, scheme string) (*mp4.InitProtectData, *mp4.InitSegment, error) {
initSeg, err := getInitSeg(rawInit)
if err != nil {
return fmt.Errorf("new uuid: %w", err)
return nil, nil, fmt.Errorf("decode init: %w", err)
}
ipd, err := mp4.InitProtect(initSeg, nil, ed.iv, "cbcs", kid, nil)
kidUUI, err := mp4.NewUUIDFromHex(hex.EncodeToString(kid[:]))
if err != nil {
return fmt.Errorf("init protect cbcs: %w", err)
return nil, nil, fmt.Errorf("new uuid: %w", err)
}
logger.Info("Generate init segment for encryption", "scheme", "cbcs", "repID", r.ID)
ed.cbcsPD = ipd
ed.cbcsInitSeg = initSeg
ed.cbcsInitBytes, err = getInitBytes(initSeg)
ipd, err := mp4.InitProtect(initSeg, nil, iv, scheme, kidUUI, nil)
if err != nil {
return fmt.Errorf("getInitBytes: %w", err)
return nil, nil, fmt.Errorf("init protect %s: %w", scheme, err)
}
return ipd, initSeg, nil
}

// Generate cenc data
initSeg, err = getInitSeg(data)
if err != nil {
return fmt.Errorf("decode init: %w", err)
func (r *RepData) addEncryption(logger *slog.Logger, assetName string) error {
logger = logger.With("init", r.InitURI)
// Set up the encryption data for this representation given asset
kid := kidFromString(assetName)
red := repEncData{
keyID: kid,
key: kidToKey(kid),
iv: defaultIV,
initEnc: make(map[string]initEncData, 2),
}
ipd, err = mp4.InitProtect(initSeg, nil, ed.iv, "cenc", kid, nil)

preEncrypted, err := checkPreEncrypted(logger, r.initBytes)
if err != nil {
return fmt.Errorf("init protect cenc: %w", err)
return fmt.Errorf("checkPreEncrypted: %w", err)
}
logger.Info("Generate init segment for encryption", "scheme", "cenc", "repID", r.ID)
ed.cencPD = ipd
ed.cencInitSeg = initSeg
ed.cencInitBytes, err = getInitBytes(initSeg)
if err != nil {
return fmt.Errorf("getInitBytes: %w", err)
if preEncrypted {
r.PreEncrypted = true
return nil
}

rawInit := r.initBytes
for _, scheme := range []string{"cbcs", "cenc"} {
initProtect, initSeg, error := genEncInit(rawInit, red.keyID, red.iv, scheme)
if error != nil {
return fmt.Errorf("genEncInit: %w", error)
}
logger.Info("Generated init segment for encryption", "scheme", scheme)
rawEncInit, err := getInitBytes(initSeg)
if err != nil {
return fmt.Errorf("getInitBytes: %w", err)
}
rd := initEncData{
scheme: scheme,
pd: initProtect,
init: initSeg,
initRaw: rawEncInit,
}
red.initEnc[scheme] = rd
}
r.encData = &ed
r.encData = &red
return nil
}

Expand Down
7 changes: 4 additions & 3 deletions cmd/livesim2/app/cmaf-ingester.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (cm *cmafIngesterMgr) NewCmafIngester(req CmafIngesterSetup) (nr uint64, er
return 0, fmt.Errorf("unknown asset %q", contentPart)
}
_, mpdName := path.Split(contentPart)
liveMPD, err := LiveMPD(asset, mpdName, cfg, nowMS)
liveMPD, err := LiveMPD(asset, mpdName, cfg, nil, nowMS)
if err != nil {
return 0, fmt.Errorf("failed to generate live MPD: %w", err)
}
Expand Down Expand Up @@ -250,7 +250,7 @@ func (c *cmafIngester) start(ctx context.Context) {
}
initBin = sw.Bytes()
} else {
match, err := matchInit(rd.initPath, c.cfg, c.asset)
match, err := matchInit(rd.initPath, c.cfg, c.mgr.s.Cfg.DrmCfg, c.asset)
if err != nil {
msg := fmt.Sprintf("Error matching init segment: %v", err)
c.report = append(c.report, msg)
Expand Down Expand Up @@ -557,7 +557,8 @@ func (c *cmafIngester) sendMediaSegment(ctx context.Context, wg *sync.WaitGroup,

// Create media segment based on number and send it to segPath
go src.startReadAndSend(ctx, finishedSendCh)
code, err := writeSegment(ctx, src, c.log, c.cfg, c.mgr.s.assetMgr.vodFS, c.asset, segPart, nowMS, c.mgr.s.textTemplates, isLast)
code, err := writeSegment(ctx, src, c.log, c.cfg, c.mgr.s.Cfg.DrmCfg, c.mgr.s.assetMgr.vodFS,
c.asset, segPart, nowMS, c.mgr.s.textTemplates, isLast)
c.log.Info("writeSegment", "code", code, "err", err)
if err != nil {
c.log.Error("writeSegment", "code", code, "err", err)
Expand Down
1 change: 1 addition & 0 deletions cmd/livesim2/app/cmaf-ingester_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestCmafIngesterMgr(t *testing.T) {
// Create a HTTP server that can receive data
// Create a new CMAF ingester with the manager
// Check that init and media segments are received.
// TODO: Add test for DRM

cfg := ServerConfig{
VodRoot: "testdata/assets",
Expand Down
39 changes: 29 additions & 10 deletions cmd/livesim2/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/koanf/providers/structs"

"github.com/Dash-Industry-Forum/livesim2/pkg/drm"
"github.com/Dash-Industry-Forum/livesim2/pkg/logging"
"github.com/spf13/pflag"
)
Expand Down Expand Up @@ -60,7 +61,9 @@ type ServerConfig struct {
Host string `json:"host"`
// PlayURL is a URL template to play asset including player and pattern %s to be replaced by MPD URL
// For autoplay, start the player muted.
PlayURL string `json:"playurl"`
PlayURL string `json:"playurl"`
DrmCfgFile string `json:"drmcfgfile"`
DrmCfg *drm.DrmConfig `json:"drmcfg"`
}

var DefaultConfig = ServerConfig{
Expand Down Expand Up @@ -126,6 +129,8 @@ func LoadConfig(args []string, cwd string) (*ServerConfig, error) {
f.String("scheme", k.String("scheme"), "scheme used in Location and BaseURL elements. If empty, it is attempted to be auto-detected")
f.String("host", k.String("host"), "host (and possible prefix) used in MPD elements. Overrides auto-detected full scheme://host")
f.String("playurl", k.String("playurl"), "URL template to play mpd. %s will be replaced by MPD URL")
f.String("drmcfgfile", k.String("drmcfgfile"), "DRM config file path")

if err := f.Parse(args[1:]); err != nil {
return nil, fmt.Errorf("command line parse: %w", err)
}
Expand Down Expand Up @@ -158,15 +163,9 @@ func LoadConfig(args []string, cwd string) (*ServerConfig, error) {
}

// Make vodPath absolute in case it is not already
vodRoot := k.String("vodroot")
if vodRoot != "" && !path.IsAbs(vodRoot) {
vodRoot = path.Join(cwd, vodRoot)
err = k.Load(confmap.Provider(map[string]any{
"vodroot": vodRoot,
}, "."), nil)
if err != nil {
return nil, err
}
vodRoot, err := makeAbsolutePath(k, "vodroot", cwd)
if err != nil {
return nil, fmt.Errorf("make vodroot absolute: %w", err)
}
// Update repDataRoot to consistent value including absolute path
repDataRoot := k.String("repdataroot")
Expand Down Expand Up @@ -215,6 +214,12 @@ func LoadConfig(args []string, cwd string) (*ServerConfig, error) {
}
}

// Make drmconfig absolute in case it is not already
_, err = makeAbsolutePath(k, "drmconfig", cwd)
if err != nil {
return nil, fmt.Errorf("make drmconfig absolute: %w", err)
}

// Unmarshal into cfg
var cfg ServerConfig
if err := k.Unmarshal("", &cfg); err != nil {
Expand All @@ -224,6 +229,20 @@ func LoadConfig(args []string, cwd string) (*ServerConfig, error) {
return &cfg, nil
}

func makeAbsolutePath(k *koanf.Koanf, key, cwd string) (string, error) {
absPath := k.String(key)
if absPath != "" && !path.IsAbs(absPath) {
absPath = path.Join(cwd, absPath)
err := k.Load(confmap.Provider(map[string]any{
key: absPath,
}, "."), nil)
if err != nil {
return "", err
}
}
return absPath, nil
}

func checkTLSParams(k *koanf.Koanf) error {
domains := k.String("domains")
certPath := k.String("certpath")
Expand Down
Loading

0 comments on commit 05dd3a7

Please sign in to comment.