Skip to content

Commit

Permalink
add PresentationDelayInspector
Browse files Browse the repository at this point in the history
  • Loading branch information
sunfish-shogi committed Mar 31, 2023
1 parent f3a8dca commit c82b671
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 18 deletions.
177 changes: 177 additions & 0 deletions inspectors/dash/presentation_delay.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package dash

import (
"time"

"github.com/abema/antares/core"
)

type PresentationDelayInspectorConfig struct {
Warn time.Duration
Error time.Duration
}

func DefaultPresentationDelayInspectorConfig() *PresentationDelayInspectorConfig {
return &PresentationDelayInspectorConfig{
Warn: 2 * time.Second,
Error: 0,
}
}

func NewPresentationDelayInspector() core.DASHInspector {
return NewPresentationDelayInspectorWithConfig(DefaultPresentationDelayInspectorConfig())
}

func NewPresentationDelayInspectorWithConfig(config *PresentationDelayInspectorConfig) core.DASHInspector {
return &PresentationDelayInspector{
config,
}
}

type PresentationDelayInspector struct {
config *PresentationDelayInspectorConfig
}

func (ins *PresentationDelayInspector) Inspect(manifest *core.Manifest, segments core.SegmentStore) *core.Report {
if manifest.Type != nil && *manifest.Type == "static" {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Info,
Message: "skip VOD manifest",
}
}

// get wall-clock
wallClock := time.Now()
if manifest.UTCTiming != nil && manifest.UTCTiming.SchemeIDURI != nil && *manifest.UTCTiming.SchemeIDURI == "urn:mpeg:dash:utc:direct:2014" {
tm, err := time.Parse(time.RFC3339Nano, *manifest.UTCTiming.Value)
if err != nil {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Error,
Message: "invalid UTCTiming@value",
Values: core.Values{"error": err},
}
}
wallClock = tm
} else if manifest.PublishTime != nil {
tm, err := time.Parse(time.RFC3339Nano, *manifest.PublishTime)
if err != nil {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Error,
Message: "invalid MPD@publishTime",
Values: core.Values{"error": err},
}
}
wallClock = tm
}

// get suggestedPresentationDelay
var suggestedPresentationDelay time.Duration
if manifest.SuggestedPresentationDelay != nil {
suggestedPresentationDelay = time.Duration(*manifest.SuggestedPresentationDelay)
}

// get availabilityStartTime
var availabilityStartTime time.Time
if manifest.AvailabilityStartTime != nil {
tm, err := time.Parse(time.RFC3339Nano, *manifest.AvailabilityStartTime)
if err != nil {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Error,
Message: "invalid MPD@availabilityStartTime",
Values: core.Values{"error": err},
}
}
availabilityStartTime = tm
}

// get latest time
var earliestVideoTime time.Time
var latestVideoTime time.Time
err := manifest.EachSegments(func(segment *core.DASHSegment) (cont bool) {
if segment.Initialization {
return true
}
var periodStart float64
if segment.Period.Start != nil {
periodStart = time.Duration(*segment.Period.Start).Seconds()
}
var offset uint64
if segment.SegmentTemplate.PresentationTimeOffset != nil {
offset = *segment.SegmentTemplate.PresentationTimeOffset
}
timescale := float64(1)
if segment.SegmentTemplate.Timescale != nil {
timescale = float64(*segment.SegmentTemplate.Timescale)
}
s := int64(segment.Time) - int64(offset)
fs := periodStart + float64(s)/timescale
ts := availabilityStartTime.Add(time.Duration(fs*1e9) * time.Nanosecond)
if earliestVideoTime.IsZero() || ts.Before(earliestVideoTime) {
earliestVideoTime = ts
}
e := s + int64(segment.Duration)
fe := periodStart + float64(e)/timescale
te := availabilityStartTime.Add(time.Duration(fe*1e9) * time.Nanosecond)
if te.After(latestVideoTime) {
latestVideoTime = te
}
return true
})
if err != nil {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Error,
Message: "unexpected error",
Values: core.Values{"error": err},
}
}

values := core.Values{
"earliestVideoTime": earliestVideoTime.UTC().Format(time.RFC3339Nano),
"latestVideoTime": latestVideoTime.UTC().Format(time.RFC3339Nano),
"wallClock": wallClock.UTC().Format(time.RFC3339Nano),
"suggestedPresentationDelay": suggestedPresentationDelay,
}
earliestRenderTime := earliestVideoTime.Add(suggestedPresentationDelay)
latestRenderTime := latestVideoTime.Add(suggestedPresentationDelay)
if earliestRenderTime.Add(ins.config.Error).After(wallClock) {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Error,
Message: "earliest segment is out of suggested time range",
Values: values,
}
} else if earliestRenderTime.Add(ins.config.Warn).After(wallClock) {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Warn,
Message: "earliest segment is out of suggested time range",
Values: values,
}
}
if latestRenderTime.Add(-ins.config.Error).Before(wallClock) {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Error,
Message: "latest segment is out of suggested time range",
Values: values,
}
} else if latestRenderTime.Add(-ins.config.Warn).Before(wallClock) {
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Warn,
Message: "latest segment is out of suggested time range",
Values: values,
}
}
return &core.Report{
Name: "PresentationDelayInspector",
Severity: core.Info,
Message: "good",
Values: values,
}
}
101 changes: 101 additions & 0 deletions inspectors/dash/presentation_delay_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package dash

import (
"testing"
"time"

"github.com/abema/antares/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zencoder/go-dash/helpers/ptrs"
"github.com/zencoder/go-dash/mpd"
)

func TestPresentationDelayInspector(t *testing.T) {
ins := NewPresentationDelayInspector()

buildManifest := func(suggestedPresentationDelay time.Duration) *core.Manifest {
periodStart := 5 * time.Hour
return &core.Manifest{
MPD: &mpd.MPD{
Type: ptrs.Strptr("dynamic"),
AvailabilityStartTime: ptrs.Strptr("2023-01-01T00:00:00Z"),
SuggestedPresentationDelay: (*mpd.Duration)(&suggestedPresentationDelay),
Periods: []*mpd.Period{{
Start: (*mpd.Duration)(&periodStart),
AdaptationSets: []*mpd.AdaptationSet{{
SegmentTemplate: &mpd.SegmentTemplate{
Timescale: ptrs.Int64ptr(90000),
PresentationTimeOffset: ptrs.Uint64ptr(10000 * 90000),
Media: ptrs.Strptr("$Time$.mp4"),
SegmentTimeline: &mpd.SegmentTimeline{
Segments: []*mpd.SegmentTimelineSegment{
{
StartTime: ptrs.Uint64ptr(13600 * 90000),
Duration: 4 * 90000,
RepeatCount: ptrs.Intptr(7),
},
},
},
},
Representations: []*mpd.Representation{{}},
}},
}},
UTCTiming: &mpd.DescriptorType{
SchemeIDURI: ptrs.Strptr("urn:mpeg:dash:utc:direct:2014"),
Value: ptrs.Strptr("2023-01-01T06:00:36Z"), // 4 seconds later from latest segment
},
},
}
}

t.Run("ok", func(t *testing.T) {
report := ins.Inspect(buildManifest(7*time.Second), nil)
require.Equal(t, core.Info, report.Severity)
assert.Equal(t, "good", report.Message)
})

t.Run("warn/presentation_time_is_new", func(t *testing.T) {
report := ins.Inspect(buildManifest(5*time.Second), nil)
require.Equal(t, core.Warn, report.Severity)
assert.Equal(t, "latest segment is out of suggested time range", report.Message)
})

t.Run("error/presentation_time_is_new", func(t *testing.T) {
report := ins.Inspect(buildManifest(3*time.Second), nil)
require.Equal(t, core.Error, report.Severity)
assert.Equal(t, "latest segment is out of suggested time range", report.Message)
})

t.Run("warn/presentation_time_is_old", func(t *testing.T) {
report := ins.Inspect(buildManifest(35*time.Second), nil)
require.Equal(t, core.Warn, report.Severity)
assert.Equal(t, "earliest segment is out of suggested time range", report.Message)
})

t.Run("error/presentation_time_is_old", func(t *testing.T) {
report := ins.Inspect(buildManifest(37*time.Second), nil)
require.Equal(t, core.Error, report.Severity)
assert.Equal(t, "earliest segment is out of suggested time range", report.Message)
})

t.Run("skip_vod_manifest", func(t *testing.T) {
report := ins.Inspect(&core.Manifest{MPD: &mpd.MPD{Type: ptrs.Strptr("static")}}, nil)
require.Equal(t, core.Info, report.Severity)
assert.Equal(t, "skip VOD manifest", report.Message)
})

t.Run("invalid_utc_timing", func(t *testing.T) {
report := ins.Inspect(&core.Manifest{
MPD: &mpd.MPD{
Type: ptrs.Strptr("dynamic"),
UTCTiming: &mpd.DescriptorType{
SchemeIDURI: ptrs.Strptr("urn:mpeg:dash:utc:direct:2014"),
Value: ptrs.Strptr("xxxxx"),
},
},
}, nil)
require.Equal(t, core.Info, report.Severity)
assert.Equal(t, "skip VOD manifest", report.Message)
})
}
37 changes: 19 additions & 18 deletions inspectors/dash/speed.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,25 @@ func (ins *speedInspector) Inspect(manifest *core.Manifest, segments core.Segmen
}
var videoTime float64
err := manifest.EachSegments(func(segment *core.DASHSegment) (cont bool) {
if !segment.Initialization {
var periodStart float64
if segment.Period.Start != nil {
periodStart = time.Duration(*segment.Period.Start).Seconds()
}
var offset uint64
if segment.SegmentTemplate.PresentationTimeOffset != nil {
offset = *segment.SegmentTemplate.PresentationTimeOffset
}
timescale := float64(1)
if segment.SegmentTemplate.Timescale != nil {
timescale = float64(*segment.SegmentTemplate.Timescale)
}
t := int64(segment.Time) - int64(offset) + int64(segment.Duration)
vt := periodStart + float64(t)/timescale
if vt > videoTime {
videoTime = vt
}
if segment.Initialization {
return true
}
var periodStart float64
if segment.Period.Start != nil {
periodStart = time.Duration(*segment.Period.Start).Seconds()
}
var offset uint64
if segment.SegmentTemplate.PresentationTimeOffset != nil {
offset = *segment.SegmentTemplate.PresentationTimeOffset
}
timescale := float64(1)
if segment.SegmentTemplate.Timescale != nil {
timescale = float64(*segment.SegmentTemplate.Timescale)
}
t := int64(segment.Time) - int64(offset) + int64(segment.Duration)
vt := periodStart + float64(t)/timescale
if vt > videoTime {
videoTime = vt
}
return true
})
Expand Down

0 comments on commit c82b671

Please sign in to comment.