Skip to content

Commit

Permalink
feat: log on mislabeled segment durations for HLS (#1010)
Browse files Browse the repository at this point in the history
  • Loading branch information
gesinger authored Dec 7, 2020
1 parent 197daab commit 4109a7f
Show file tree
Hide file tree
Showing 2 changed files with 335 additions and 1 deletion.
98 changes: 98 additions & 0 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,93 @@ export const shouldWaitForTimelineChange = ({
return false;
};

export const mediaDuration = (audioTimingInfo, videoTimingInfo) => {
const audioDuration =
audioTimingInfo &&
typeof audioTimingInfo.start === 'number' &&
typeof audioTimingInfo.end === 'number' ?
audioTimingInfo.end - audioTimingInfo.start : 0;
const videoDuration =
videoTimingInfo &&
typeof videoTimingInfo.start === 'number' &&
typeof videoTimingInfo.end === 'number' ?
videoTimingInfo.end - videoTimingInfo.start : 0;

return Math.max(audioDuration, videoDuration);
};

export const segmentTooLong = ({ segmentDuration, maxDuration }) => {
// 0 duration segments are most likely due to metadata only segments or a lack of
// information.
if (!segmentDuration) {
return false;
}

// For HLS:
//
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1
// The EXTINF duration of each Media Segment in the Playlist
// file, when rounded to the nearest integer, MUST be less than or equal
// to the target duration; longer segments can trigger playback stalls
// or other errors.
//
// For DASH, the mpd-parser uses the largest reported segment duration as the target
// duration. Although that reported duration is occasionally approximate (i.e., not
// exact), a strict check may report that a segment is too long more often in DASH.
return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR;
};

export const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => {
// Right now we aren't following DASH's timing model exactly, so only perform
// this check for HLS content.
if (sourceType !== 'hls') {
return null;
}

const segmentDuration = mediaDuration(
segmentInfo.audioTimingInfo,
segmentInfo.videoTimingInfo
);

// Don't report if we lack information.
//
// If the segment has a duration of 0 it is either a lack of information or a
// metadata only segment and shouldn't be reported here.
if (!segmentDuration) {
return null;
}

const targetDuration = segmentInfo.playlist.targetDuration;

const isSegmentWayTooLong = segmentTooLong({
segmentDuration,
maxDuration: targetDuration * 2
});
const isSegmentSlightlyTooLong = segmentTooLong({
segmentDuration,
maxDuration: targetDuration
});

const segmentTooLongMessage = `Segment with index ${segmentInfo.mediaIndex} ` +
`from playlist ${segmentInfo.playlist.id} ` +
`has a duration of ${segmentDuration} ` +
`when the reported duration is ${segmentInfo.duration} ` +
`and the target duration is ${targetDuration}. ` +
'For HLS content, a duration in excess of the target duration may result in ' +
'playback issues. See the HLS specification section on EXT-X-TARGETDURATION for ' +
'more details: ' +
'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1';

if (isSegmentWayTooLong || isSegmentSlightlyTooLong) {
return {
severity: isSegmentWayTooLong ? 'warn' : 'info',
message: segmentTooLongMessage
};
}

return null;
};

/**
* An object that manages segment loading and appending.
*
Expand Down Expand Up @@ -2614,6 +2701,17 @@ export default class SegmentLoader extends videojs.EventTarget {

this.logger_(segmentInfoString(segmentInfo));

const segmentDurationMessage =
getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_);

if (segmentDurationMessage) {
if (segmentDurationMessage.severity === 'warn') {
videojs.log.warn(segmentDurationMessage.message);
} else {
this.logger_(segmentDurationMessage.message);
}
}

this.recordThroughput_(segmentInfo);
this.pendingSegment_ = null;
this.state = 'READY';
Expand Down
238 changes: 237 additions & 1 deletion test/segment-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
illegalMediaSwitch,
safeBackBufferTrimTime,
timestampOffsetForSegment,
shouldWaitForTimelineChange
shouldWaitForTimelineChange,
segmentTooLong,
mediaDuration,
getTroublesomeSegmentDurationMessage
} from '../src/segment-loader';
import segmentTransmuxer from '../src/segment-transmuxer';
import videojs from 'video.js';
Expand Down Expand Up @@ -481,6 +484,239 @@ QUnit.test('uses current time when seekable range is well before current time',
);
});

QUnit.module('mediaDuration');

QUnit.test('0 when no timing info', function(assert) {
assert.equal(mediaDuration({}, {}), 0, '0 when no timing info');
assert.equal(mediaDuration({ start: 1 }, { start: 1 }), 0, '0 when no end times');
assert.equal(mediaDuration({ end: 1 }, { end: 1 }), 0, '0 when no start times');
});

QUnit.test('reports audio duration', function(assert) {
assert.equal(
mediaDuration({ start: 1, end: 2 }, {}),
1,
'audio duration when no video info'
);

assert.equal(
mediaDuration({ start: 1, end: 2 }, { start: 1 }),
1,
'audio duration when not enough video info'
);

assert.equal(
mediaDuration({ start: 1, end: 2 }, { end: 3 }),
1,
'audio duration when not enough video info'
);

assert.equal(
mediaDuration({ start: 1, end: 3 }, { start: 1, end: 2 }),
2,
'audio duration when audio duration > video duration'
);
});

QUnit.test('reports video duration', function(assert) {
assert.equal(
mediaDuration({}, { start: 1, end: 2 }),
1,
'video duration when no audio info'
);

assert.equal(
mediaDuration({ start: 1 }, { start: 1, end: 2 }),
1,
'video duration when not enough audio info'
);

assert.equal(
mediaDuration({ end: 3 }, { start: 1, end: 2 }),
1,
'video duration when not enough audio info'
);

assert.equal(
mediaDuration({ start: 1, end: 2 }, { start: 1, end: 3 }),
2,
'video duration when video duration > audio duration'
);
});

QUnit.module('segmentTooLong');

QUnit.test('false when no segment duration', function(assert) {
assert.notOk(segmentTooLong({ maxDuration: 9 }), 'false when no segment duration');
assert.notOk(
segmentTooLong({ segmentDuration: 0, maxDuration: 9 }),
'false when segment duration is 0'
);
});

QUnit.test('false when duration is within range', function(assert) {
assert.notOk(
segmentTooLong({
segmentDuration: 9,
maxDuration: 9
}),
'false when duration is same'
);
assert.notOk(
segmentTooLong({
segmentDuration: 9.49,
maxDuration: 9
}),
'false when duration rounds down to same'
);
});

QUnit.test('true when duration is too long', function(assert) {
assert.ok(
segmentTooLong({
segmentDuration: 9,
maxDuration: 8.9
}),
'true when duration is too long'
);
assert.ok(
segmentTooLong({
segmentDuration: 9.5,
maxDuration: 9
}),
'true when duration rounds up to be too long'
);
});

QUnit.module('getTroublesomeSegmentDurationMessage');

QUnit.test('falsey when dash', function(assert) {
assert.notOk(
getTroublesomeSegmentDurationMessage(
{
audioTimingInfo: { start: 0, end: 10 },
videoTimingInfo: { start: 0, end: 10 },
mediaIndex: 0,
playlist: {
id: 'id',
targetDuration: 4
}
},
'dash'
),
'falsey when dash'
);
});

QUnit.test('falsey when segment is within range', function(assert) {
assert.notOk(
getTroublesomeSegmentDurationMessage(
{
audioTimingInfo: { start: 0, end: 10 },
videoTimingInfo: { start: 0, end: 10 },
duration: 10,
mediaIndex: 0,
playlist: {
id: 'id',
targetDuration: 10
}
},
'hls'
),
'falsey when segment equal to target duration'
);

assert.notOk(
getTroublesomeSegmentDurationMessage(
{
audioTimingInfo: { start: 0, end: 10 },
videoTimingInfo: { start: 0, end: 5 },
duration: 10,
mediaIndex: 0,
playlist: {
id: 'id',
targetDuration: 10
}
},
'hls'
),
'falsey when segment less than target duration'
);

assert.notOk(
getTroublesomeSegmentDurationMessage(
{
audioTimingInfo: { start: 0, end: 5 },
videoTimingInfo: { start: 0, end: 5 },
mediaIndex: 0,
duration: 5,
playlist: {
id: 'id',
targetDuration: 10
}
},
'hls'
),
'falsey when segment less than target duration'
);
});

QUnit.test('warn when segment is way too long', function(assert) {
assert.deepEqual(
getTroublesomeSegmentDurationMessage(
{
audioTimingInfo: { start: 0, end: 10 },
videoTimingInfo: { start: 0, end: 10 },
mediaIndex: 0,
duration: 10,
playlist: {
targetDuration: 4,
id: 'id'
}
},
'hls'
),
{
severity: 'warn',
message:
'Segment with index 0 from playlist id has a duration of 10 when the reported ' +
'duration is 10 and the target duration is 4. For HLS content, a duration in ' +
'excess of the target duration may result in playback issues. See the HLS ' +
'specification section on EXT-X-TARGETDURATION for more details: ' +
'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1'
},
'warn when segment way too long'
);
});

QUnit.test('info segment is bit too long', function(assert) {
assert.deepEqual(
getTroublesomeSegmentDurationMessage(
{
audioTimingInfo: { start: 0, end: 4.5 },
videoTimingInfo: { start: 0, end: 4.5 },
mediaIndex: 0,
duration: 4.5,
playlist: {
id: 'id',
targetDuration: 4
}
},
'hls'
),
{
severity: 'info',
message:
'Segment with index 0 from playlist id has a duration of 4.5 when the reported ' +
'duration is 4.5 and the target duration is 4. For HLS content, a duration in ' +
'excess of the target duration may result in playback issues. See the HLS ' +
'specification section on EXT-X-TARGETDURATION for more details: ' +
'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1'
},
'info when segment is a bit too long'
);
});

QUnit.module('SegmentLoader', function(hooks) {
hooks.beforeEach(LoaderCommonHooks.beforeEach);
hooks.afterEach(LoaderCommonHooks.afterEach);
Expand Down

0 comments on commit 4109a7f

Please sign in to comment.