You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
684 lines
16 KiB
684 lines
16 KiB
import Playlist from '../src/playlist';
|
|
import PlaylistLoader from '../src/playlist-loader';
|
|
import QUnit from 'qunit';
|
|
import xhrFactory from '../src/xhr';
|
|
import { useFakeEnvironment } from './test-helpers';
|
|
|
|
QUnit.module('Playlist Duration');
|
|
|
|
QUnit.test('total duration for live playlists is Infinity', function(assert) {
|
|
let duration = Playlist.duration({
|
|
segments: [{
|
|
duration: 4,
|
|
uri: '0.ts'
|
|
}]
|
|
});
|
|
|
|
assert.equal(duration, Infinity, 'duration is infinity');
|
|
});
|
|
|
|
QUnit.module('Playlist Interval Duration');
|
|
|
|
QUnit.test('accounts for non-zero starting VOD media sequences', function(assert) {
|
|
let duration = Playlist.duration({
|
|
mediaSequence: 10,
|
|
endList: true,
|
|
segments: [{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
}]
|
|
});
|
|
|
|
assert.equal(duration, 4 * 10, 'includes only listed segments');
|
|
});
|
|
|
|
QUnit.test('uses timeline values when available', function(assert) {
|
|
let duration = Playlist.duration({
|
|
mediaSequence: 0,
|
|
endList: true,
|
|
segments: [{
|
|
start: 0,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 10,
|
|
end: 2 * 10 + 2,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 10,
|
|
end: 3 * 10 + 2,
|
|
uri: '2.ts'
|
|
}, {
|
|
duration: 10,
|
|
end: 4 * 10 + 2,
|
|
uri: '3.ts'
|
|
}]
|
|
}, 4);
|
|
|
|
assert.equal(duration, 4 * 10 + 2, 'used timeline values');
|
|
});
|
|
|
|
QUnit.test('works when partial timeline information is available', function(assert) {
|
|
let duration = Playlist.duration({
|
|
mediaSequence: 0,
|
|
endList: true,
|
|
segments: [{
|
|
start: 0,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 9,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
}, {
|
|
duration: 10,
|
|
start: 30.007,
|
|
end: 40.002,
|
|
uri: '3.ts'
|
|
}, {
|
|
duration: 10,
|
|
end: 50.0002,
|
|
uri: '4.ts'
|
|
}]
|
|
}, 5);
|
|
|
|
assert.equal(duration, 50.0002, 'calculated with mixed intervals');
|
|
});
|
|
|
|
QUnit.test('uses timeline values for the expired duration of live playlists', function(assert) {
|
|
let playlist = {
|
|
mediaSequence: 12,
|
|
segments: [{
|
|
duration: 10,
|
|
end: 120.5,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 9,
|
|
uri: '1.ts'
|
|
}]
|
|
};
|
|
let duration;
|
|
|
|
duration = Playlist.duration(playlist, playlist.mediaSequence);
|
|
assert.equal(duration, 110.5, 'used segment end time');
|
|
duration = Playlist.duration(playlist, playlist.mediaSequence + 1);
|
|
assert.equal(duration, 120.5, 'used segment end time');
|
|
duration = Playlist.duration(playlist, playlist.mediaSequence + 2);
|
|
assert.equal(duration, 120.5 + 9, 'used segment end time');
|
|
});
|
|
|
|
QUnit.test('looks outside the queried interval for live playlist timeline values',
|
|
function(assert) {
|
|
let playlist = {
|
|
mediaSequence: 12,
|
|
segments: [{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 9,
|
|
end: 120.5,
|
|
uri: '1.ts'
|
|
}]
|
|
};
|
|
let duration;
|
|
|
|
duration = Playlist.duration(playlist, playlist.mediaSequence);
|
|
assert.equal(duration, 120.5 - 9 - 10, 'used segment end time');
|
|
});
|
|
|
|
QUnit.test('ignores discontinuity sequences later than the end', function(assert) {
|
|
let duration = Playlist.duration({
|
|
mediaSequence: 0,
|
|
discontinuityStarts: [1, 3],
|
|
segments: [{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
discontinuity: true,
|
|
duration: 9,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
}, {
|
|
discontinuity: true,
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
}]
|
|
}, 2);
|
|
|
|
assert.equal(duration, 19, 'excluded the later segments');
|
|
});
|
|
|
|
QUnit.test('handles trailing segments without timeline information', function(assert) {
|
|
let duration;
|
|
let playlist = {
|
|
mediaSequence: 0,
|
|
endList: true,
|
|
segments: [{
|
|
start: 0,
|
|
end: 10.5,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 9,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
}, {
|
|
start: 29.45,
|
|
end: 39.5,
|
|
uri: '3.ts'
|
|
}]
|
|
};
|
|
|
|
duration = Playlist.duration(playlist, 3);
|
|
assert.equal(duration, 29.45, 'calculated duration');
|
|
|
|
duration = Playlist.duration(playlist, 2);
|
|
assert.equal(duration, 19.5, 'calculated duration');
|
|
});
|
|
|
|
QUnit.test('uses timeline intervals when segments have them', function(assert) {
|
|
let duration;
|
|
let playlist = {
|
|
mediaSequence: 0,
|
|
segments: [{
|
|
start: 0,
|
|
end: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 9,
|
|
uri: '1.ts'
|
|
}, {
|
|
start: 20.1,
|
|
end: 30.1,
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
}]
|
|
};
|
|
|
|
duration = Playlist.duration(playlist, 2);
|
|
assert.equal(duration, 20.1, 'used the timeline-based interval');
|
|
|
|
duration = Playlist.duration(playlist, 3);
|
|
assert.equal(duration, 30.1, 'used the timeline-based interval');
|
|
});
|
|
|
|
QUnit.test('counts the time between segments as part of the earlier segment\'s duration',
|
|
function(assert) {
|
|
let duration = Playlist.duration({
|
|
mediaSequence: 0,
|
|
endList: true,
|
|
segments: [{
|
|
start: 0,
|
|
end: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
start: 10.1,
|
|
end: 20.1,
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
}]
|
|
}, 1);
|
|
|
|
assert.equal(duration, 10.1, 'included the segment gap');
|
|
});
|
|
|
|
QUnit.test('accounts for discontinuities', function(assert) {
|
|
let duration = Playlist.duration({
|
|
mediaSequence: 0,
|
|
endList: true,
|
|
discontinuityStarts: [1],
|
|
segments: [{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
discontinuity: true,
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
}]
|
|
}, 2);
|
|
|
|
assert.equal(duration, 10 + 10, 'handles discontinuities');
|
|
});
|
|
|
|
QUnit.test('a non-positive length interval has zero duration', function(assert) {
|
|
let playlist = {
|
|
mediaSequence: 0,
|
|
discontinuityStarts: [1],
|
|
segments: [{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
discontinuity: true,
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
}]
|
|
};
|
|
|
|
assert.equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero');
|
|
assert.equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero');
|
|
assert.equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero');
|
|
});
|
|
|
|
QUnit.module('Playlist Seekable');
|
|
|
|
QUnit.test('calculates seekable time ranges from the available segments', function(assert) {
|
|
let playlist = {
|
|
mediaSequence: 0,
|
|
segments: [{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
}],
|
|
endList: true
|
|
};
|
|
let seekable = Playlist.seekable(playlist);
|
|
|
|
assert.equal(seekable.length, 1, 'there are seekable ranges');
|
|
assert.equal(seekable.start(0), 0, 'starts at zero');
|
|
assert.equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
|
|
});
|
|
|
|
QUnit.test('master playlists have empty seekable ranges', function(assert) {
|
|
let seekable = Playlist.seekable({
|
|
playlists: [{
|
|
uri: 'low.m3u8'
|
|
}, {
|
|
uri: 'high.m3u8'
|
|
}]
|
|
});
|
|
|
|
assert.equal(seekable.length, 0, 'no seekable ranges from a master playlist');
|
|
});
|
|
|
|
QUnit.test('seekable end is three target durations from the actual end of live playlists',
|
|
function(assert) {
|
|
let seekable = Playlist.seekable({
|
|
mediaSequence: 0,
|
|
syncInfo: {
|
|
time: 0,
|
|
mediaSequence: 0
|
|
},
|
|
targetDuration: 10,
|
|
segments: [{
|
|
duration: 7,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
}]
|
|
});
|
|
|
|
assert.equal(seekable.length, 1, 'there are seekable ranges');
|
|
assert.equal(seekable.start(0), 0, 'starts at zero');
|
|
assert.equal(seekable.end(0), 7, 'ends three target durations from the last segment');
|
|
});
|
|
|
|
QUnit.test('seekable end accounts for non-standard target durations', function(assert) {
|
|
let seekable = Playlist.seekable({
|
|
targetDuration: 2,
|
|
mediaSequence: 0,
|
|
syncInfo: {
|
|
time: 0,
|
|
mediaSequence: 0
|
|
},
|
|
segments: [{
|
|
duration: 2,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 2,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 1,
|
|
uri: '2.ts'
|
|
}, {
|
|
duration: 2,
|
|
uri: '3.ts'
|
|
}, {
|
|
duration: 2,
|
|
uri: '4.ts'
|
|
}]
|
|
});
|
|
|
|
assert.equal(seekable.start(0), 0, 'starts at the earliest available segment');
|
|
assert.equal(seekable.end(0),
|
|
9 - (2 + 2 + 1),
|
|
'allows seeking no further than three segments from the end');
|
|
});
|
|
|
|
QUnit.test('playlist with no sync points has empty seekable range', function(assert) {
|
|
let seekable = Playlist.seekable({
|
|
targetDuration: 10,
|
|
mediaSequence: 0,
|
|
segments: [{
|
|
duration: 7,
|
|
uri: '0.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
}, {
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
}]
|
|
});
|
|
|
|
assert.equal(seekable.length, 0, 'no seekable range for playlist with no sync points');
|
|
});
|
|
|
|
QUnit.test('seekable uses available sync points for calculating seekable range',
|
|
function(assert) {
|
|
let seekable;
|
|
|
|
seekable = Playlist.seekable({
|
|
targetDuration: 10,
|
|
mediaSequence: 100,
|
|
syncInfo: {
|
|
time: 50,
|
|
mediaSequence: 95
|
|
},
|
|
segments: [
|
|
{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '4.ts'
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.ok(seekable.length, 'seekable range calculated');
|
|
assert.equal(seekable.start(0), 100, 'estimated start time based on expired sync point');
|
|
assert.equal(seekable.end(0), 120, 'allows seeking no further than three segments from the end');
|
|
|
|
seekable = Playlist.seekable({
|
|
targetDuration: 10,
|
|
mediaSequence: 100,
|
|
segments: [
|
|
{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '1.ts',
|
|
start: 108.5,
|
|
end: 118.4
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '4.ts'
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.ok(seekable.length, 'seekable range calculated');
|
|
assert.equal(seekable.start(0), 98.5, 'estimated start time using segmentSync');
|
|
assert.equal(seekable.end(0), 118.4, 'allows seeking no further than three segments from the end');
|
|
|
|
seekable = Playlist.seekable({
|
|
targetDuration: 10,
|
|
mediaSequence: 100,
|
|
syncInfo: {
|
|
time: 50,
|
|
mediaSequence: 95
|
|
},
|
|
segments: [
|
|
{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '1.ts',
|
|
start: 108.5,
|
|
end: 118.5
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '2.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '4.ts'
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.ok(seekable.length, 'seekable range calculated');
|
|
assert.equal(seekable.start(0), 98.5, 'estimated start time using nearest sync point (segmentSync in this case)');
|
|
assert.equal(seekable.end(0), 118.5, 'allows seeking no further than three segments from the end');
|
|
|
|
seekable = Playlist.seekable({
|
|
targetDuration: 10,
|
|
mediaSequence: 100,
|
|
syncInfo: {
|
|
time: 90.8,
|
|
mediaSequence: 99
|
|
},
|
|
segments: [
|
|
{
|
|
duration: 10,
|
|
uri: '0.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '1.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '2.ts',
|
|
start: 118.5,
|
|
end: 128.5
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '3.ts'
|
|
},
|
|
{
|
|
duration: 10,
|
|
uri: '4.ts'
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.ok(seekable.length, 'seekable range calculated');
|
|
assert.equal(seekable.start(0), 100.8, 'estimated start time using nearest sync point (expiredSync in this case)');
|
|
assert.equal(seekable.end(0), 118.5, 'allows seeking no further than three segments from the end');
|
|
});
|
|
|
|
QUnit.module('Playlist Media Index For Time', {
|
|
beforeEach(assert) {
|
|
this.env = useFakeEnvironment(assert);
|
|
this.clock = this.env.clock;
|
|
this.requests = this.env.requests;
|
|
this.fakeHls = {
|
|
xhr: xhrFactory()
|
|
};
|
|
},
|
|
afterEach() {
|
|
this.env.restore();
|
|
}
|
|
});
|
|
|
|
QUnit.test('can get media index by playback position for non-live videos', function(assert) {
|
|
let media;
|
|
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
|
|
|
loader.load();
|
|
|
|
this.requests.shift().respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-MEDIA-SEQUENCE:0\n' +
|
|
'#EXTINF:4,\n' +
|
|
'0.ts\n' +
|
|
'#EXTINF:5,\n' +
|
|
'1.ts\n' +
|
|
'#EXTINF:6,\n' +
|
|
'2.ts\n' +
|
|
'#EXT-X-ENDLIST\n'
|
|
);
|
|
|
|
media = loader.media();
|
|
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, -1, 0, 0).mediaIndex, 0,
|
|
'the index is never less than zero');
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, 0, 0, 0).mediaIndex, 0,
|
|
'time zero is index zero');
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, 3, 0, 0).mediaIndex, 0,
|
|
'time three is index zero');
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, 10, 0, 0).mediaIndex, 2,
|
|
'time 10 is index 2');
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, 22, 0, 0).mediaIndex, 2,
|
|
'time greater than the length is index 2');
|
|
});
|
|
|
|
QUnit.test('returns the lower index when calculating for a segment boundary', function(assert) {
|
|
let media;
|
|
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
|
|
|
loader.load();
|
|
|
|
this.requests.shift().respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-MEDIA-SEQUENCE:0\n' +
|
|
'#EXTINF:4,\n' +
|
|
'0.ts\n' +
|
|
'#EXTINF:5,\n' +
|
|
'1.ts\n' +
|
|
'#EXT-X-ENDLIST\n'
|
|
);
|
|
|
|
media = loader.media();
|
|
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, 4, 0, 0).mediaIndex, 0,
|
|
'rounds down exact matches');
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, 3.7, 0, 0).mediaIndex, 0,
|
|
'rounds down');
|
|
assert.equal(Playlist.getMediaInfoForTime_(media, 4.5, 0, 0).mediaIndex, 1,
|
|
'rounds up at 0.5');
|
|
});
|
|
|
|
QUnit.test(
|
|
'accounts for non-zero starting segment time when calculating media index',
|
|
function(assert) {
|
|
let media;
|
|
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
|
|
|
loader.load();
|
|
|
|
this.requests.shift().respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
|
|
'#EXTINF:4,\n' +
|
|
'1001.ts\n' +
|
|
'#EXTINF:5,\n' +
|
|
'1002.ts\n'
|
|
);
|
|
|
|
media = loader.media();
|
|
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 45, 0, 150).mediaIndex,
|
|
0,
|
|
'expired content returns 0 for earliest segment available'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 75, 0, 150).mediaIndex,
|
|
0,
|
|
'expired content returns 0 for earliest segment available'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 0, 0, 150).mediaIndex,
|
|
0,
|
|
'time of 0 with no expired time returns first segment'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 50 + 100, 0, 150).mediaIndex,
|
|
0,
|
|
'calculates the earliest available position'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 50 + 100 + 2, 0, 150).mediaIndex,
|
|
0,
|
|
'calculates within the first segment'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 50 + 100 + 2, 0, 150).mediaIndex,
|
|
0,
|
|
'calculates within the first segment'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 50 + 100 + 4, 0, 150).mediaIndex,
|
|
0,
|
|
'calculates earlier segment on exact boundary match'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 50 + 100 + 4.5, 0, 150).mediaIndex,
|
|
1,
|
|
'calculates within the second segment'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 50 + 100 + 6, 0, 150).mediaIndex,
|
|
1,
|
|
'calculates within the second segment'
|
|
);
|
|
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 159, 0, 150).mediaIndex,
|
|
1,
|
|
'returns last segment when time is equal to end of last segment'
|
|
);
|
|
assert.equal(
|
|
Playlist.getMediaInfoForTime_(media, 160, 0, 150).mediaIndex,
|
|
1,
|
|
'returns last segment when time is past end of last segment'
|
|
);
|
|
});
|