github.com/puellanivis/breton@v0.2.16/lib/net/dash/manifest.go (about)

     1  // Package dash provides an abstraction to accessing DASH streams.
     2  package dash
     3  
     4  import (
     5  	"context"
     6  	"io"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/puellanivis/breton/lib/files"
    11  	"github.com/puellanivis/breton/lib/glog"
    12  	"github.com/puellanivis/breton/lib/net/dash/mpd"
    13  
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  type adaptation struct {
    18  	// indexes into the period and adaptions
    19  	pid string
    20  	aid uint
    21  
    22  	base string
    23  
    24  	// template strings
    25  	init, media string
    26  	startNum    uint
    27  
    28  	reps []*mpd.Representation
    29  }
    30  
    31  // A Manifest holds the essential identifying information about a DASH manifest it is used to generate Streams.
    32  type Manifest struct {
    33  	base     string
    34  	manifest string
    35  
    36  	metrics *metricsPack
    37  
    38  	dynamic     bool
    39  	adaptations map[string]*adaptation
    40  
    41  	m *cachedMPD
    42  }
    43  
    44  // IsDynamic returns true if the Manifest describes a dynamic stream.
    45  func (m *Manifest) IsDynamic() bool {
    46  	return m.dynamic
    47  }
    48  
    49  func updateBase(ctx context.Context, baseURL []*mpd.BaseURL) context.Context {
    50  	for _, url := range baseURL {
    51  		if url.CDATA == "" {
    52  			continue
    53  		}
    54  
    55  		base := url.CDATA
    56  
    57  		ctx, err := files.WithRoot(ctx, base)
    58  		if err != nil {
    59  			glog.Fatal("omg, root failed", err)
    60  			continue
    61  		}
    62  
    63  		return ctx
    64  	}
    65  
    66  	return ctx
    67  }
    68  
    69  // New returns a Manifest constructed from the given URL.
    70  func New(ctx context.Context, manifest string) (*Manifest, error) {
    71  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    72  	defer cancel()
    73  
    74  	m, err := readMPD(ctx, manifest)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	url := urlLabel.WithValue(manifest)
    80  
    81  	idx := strings.LastIndexByte(manifest, '/')
    82  	fqManifest := manifest
    83  	base, manifest := manifest[:idx+1], manifest[idx+1:]
    84  
    85  	ctx, err = files.WithRoot(ctx, base)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	ctx = updateBase(ctx, m.BaseURL)
    91  
    92  	adaptations := make(map[string]*adaptation)
    93  
    94  	for _, p := range m.Period {
    95  		pid := p.Id
    96  
    97  		ctx := updateBase(ctx, p.BaseURL)
    98  
    99  		for _, as := range p.AdaptationSet {
   100  			ctx := updateBase(ctx, as.BaseURL)
   101  
   102  			if root, ok := files.GetRoot(ctx); ok {
   103  				base = root
   104  			}
   105  
   106  			a := &adaptation{
   107  				pid:  pid,
   108  				aid:  as.Id,
   109  				base: base,
   110  			}
   111  
   112  			if tmpl := as.SegmentTemplate; tmpl != nil {
   113  				a.init = tmpl.Initialization
   114  				a.media = tmpl.Media
   115  			}
   116  
   117  			for _, r := range as.Representation {
   118  				if as.MimeType == "" {
   119  					as.MimeType = r.MimeType
   120  				}
   121  
   122  				a.reps = append(a.reps, r)
   123  			}
   124  
   125  			if as.MimeType == "" {
   126  				continue
   127  			}
   128  
   129  			if _, ok := adaptations[as.MimeType]; ok {
   130  				continue
   131  			}
   132  
   133  			adaptations[as.MimeType] = a
   134  		}
   135  	}
   136  
   137  	minTime, err := m.MinimumUpdatePeriod.Duration()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	// I can’t imagine using a minimum time of less than one millisecond.
   143  	if minTime < time.Millisecond {
   144  		minTime = time.Millisecond
   145  	}
   146  
   147  	return &Manifest{
   148  		base:        base,
   149  		manifest:    manifest,
   150  		dynamic:     m.Type == "dynamic",
   151  		adaptations: adaptations,
   152  		m:           newMPD(fqManifest, minTime),
   153  
   154  		metrics: baseMetrics.WithLabels(url),
   155  	}, nil
   156  }
   157  
   158  // MinimumUpdatePeriod returns the shortest period within with a Manifest’s
   159  // information is to update.
   160  func (m *Manifest) MinimumUpdatePeriod() time.Duration {
   161  	return m.m.expTime
   162  }
   163  
   164  // Stream returns a Stream object that specifies a specific series of segments within the DASH manifest.
   165  func (m *Manifest) Stream(w io.Writer, mimeType string, picker Picker) (*Stream, error) {
   166  	if picker == nil {
   167  		picker = PickFirst()
   168  	}
   169  
   170  	as, ok := m.adaptations[mimeType]
   171  	if !ok {
   172  		return nil, errors.New("mime-type not available")
   173  	}
   174  
   175  	var picked *mpd.Representation
   176  
   177  	for _, rep := range as.reps {
   178  		// I don’t know how this could end up being here,
   179  		// but let’s discard it regardless.
   180  		if rep == nil {
   181  			continue
   182  		}
   183  
   184  		picked = picker(rep)
   185  	}
   186  
   187  	if picked == nil {
   188  		return nil, errors.New("no suitable representations found")
   189  	}
   190  
   191  	if glog.V(1) {
   192  		glog.Infof("chose %s with bandwidth: %d", mimeType, picked.Bandwidth)
   193  		if picked.Height > 0 && picked.Width > 0 {
   194  			glog.Infof("chose %s with %d×%d", mimeType, picked.Height, picked.Width)
   195  		}
   196  	}
   197  
   198  	return &Stream{
   199  		w: w,
   200  		m: m,
   201  
   202  		metrics: m.metrics.WithLabels(typeLabel.WithValue(mimeType)),
   203  
   204  		pid: as.pid,
   205  		aid: as.aid,
   206  
   207  		dynamic: m.dynamic,
   208  		base:    as.base,
   209  		init:    as.init,
   210  		media:   as.media,
   211  
   212  		bw:    picked.Bandwidth,
   213  		repID: string(picked.Id),
   214  	}, nil
   215  }