mirror of
https://github.com/video-dev/hls.js.git
synced 2026-06-16 13:34:44 +00:00
551 lines
15 KiB
TypeScript
551 lines
15 KiB
TypeScript
import chai from 'chai';
|
|
import sinon from 'sinon';
|
|
import sinonChai from 'sinon-chai';
|
|
import { Events } from '../../../src/events';
|
|
import Hls from '../../../src/hls';
|
|
import { Fragment } from '../../../src/loader/fragment';
|
|
import { LevelDetails } from '../../../src/loader/level-details';
|
|
import { LoadStats } from '../../../src/loader/load-stats';
|
|
import { Level } from '../../../src/types/level';
|
|
import { PlaylistLevelType } from '../../../src/types/loader';
|
|
import { AttrList } from '../../../src/utils/attr-list';
|
|
import type AbrController from '../../../src/controller/abr-controller';
|
|
import type { HlsListeners } from '../../../src/events';
|
|
import type { LevelParsed } from '../../../src/types/level';
|
|
import type {
|
|
MediaAttributes,
|
|
MediaPlaylist,
|
|
} from '../../../src/types/media-playlist';
|
|
|
|
chai.use(sinonChai);
|
|
const expect = chai.expect;
|
|
|
|
function levelDetailsWithDuration(duration: number) {
|
|
const details = new LevelDetails('');
|
|
details.totalduration = duration;
|
|
const frag = new Fragment(PlaylistLevelType.MAIN, '');
|
|
frag.url = 'foo';
|
|
details.fragments.push();
|
|
return details;
|
|
}
|
|
|
|
describe('AbrController', function () {
|
|
const sandbox = sinon.createSandbox();
|
|
|
|
let hls: Hls;
|
|
let abrController: AbrController;
|
|
|
|
beforeEach(function () {
|
|
hls = new Hls({
|
|
maxStarvationDelay: 4,
|
|
// debug: true
|
|
});
|
|
abrController = (hls as any).abrController;
|
|
});
|
|
|
|
afterEach(function () {
|
|
sandbox.restore();
|
|
hls.destroy();
|
|
});
|
|
|
|
describe('resetEstimator', function () {
|
|
it('estimate can be reset with new value', function () {
|
|
const { abrEwmaDefaultEstimate } = hls.config;
|
|
expect(abrController.bwEstimator.getEstimate()).to.equal(
|
|
abrEwmaDefaultEstimate,
|
|
);
|
|
const updatedEstimate = 5e6 + 1;
|
|
abrController.resetEstimator(updatedEstimate);
|
|
expect(abrController.bwEstimator.getEstimate()).to.equal(updatedEstimate);
|
|
});
|
|
});
|
|
|
|
describe('firstAutoLevel getter', function () {
|
|
it('returns starting level index matching estimate', function () {
|
|
(hls as any).levelController._levels = getSimpleLevels();
|
|
const bwe = abrController.bwEstimator.getEstimate();
|
|
expect(bwe).to.equal(5e5);
|
|
const firstAutoLevel = abrController.firstAutoLevel;
|
|
const level = hls.levels[firstAutoLevel];
|
|
expect(level.bitrate).to.be.lessThanOrEqual(bwe);
|
|
expect(firstAutoLevel).to.equal(2);
|
|
});
|
|
|
|
it('returns starting level with preferred codecs within estimate', function () {
|
|
(hls as any).levelController._levels = getMultiCodecLevels();
|
|
const bwe = abrController.bwEstimator.getEstimate();
|
|
expect(bwe).to.equal(5e5);
|
|
const firstAutoLevel = abrController.firstAutoLevel;
|
|
const level = hls.levels[firstAutoLevel];
|
|
expect(level.codecSet).to.equal('hevc,mp4a');
|
|
expect(level.bitrate).to.be.lessThanOrEqual(bwe);
|
|
expect(firstAutoLevel).to.equal(3);
|
|
});
|
|
|
|
it('returns starting level between minAutoLevel and maxAutoLevel', function () {
|
|
(hls as any).levelController._levels = getSimpleLevels();
|
|
expect(hls.minAutoLevel).to.equal(0);
|
|
expect(hls.maxAutoLevel).to.equal(hls.levels.length - 1);
|
|
|
|
hls.firstLevel = 4;
|
|
abrController.resetEstimator(hls.levels[hls.firstLevel].bitrate);
|
|
expect(abrController.firstAutoLevel).to.equal(
|
|
4,
|
|
'firstAutoLevel exact match',
|
|
);
|
|
|
|
hls.autoLevelCapping = 3;
|
|
hls.config.minAutoBitrate = 460560;
|
|
expect(hls.minAutoLevel).to.equal(2);
|
|
expect(hls.maxAutoLevel).to.equal(3);
|
|
expect(abrController.firstAutoLevel).to.equal(
|
|
3,
|
|
'firstAutoLevel capped to 3',
|
|
);
|
|
|
|
hls.autoLevelCapping = 0;
|
|
hls.config.minAutoBitrate = 0;
|
|
expect(hls.minAutoLevel).to.equal(0);
|
|
expect(hls.maxAutoLevel).to.equal(0);
|
|
expect(abrController.firstAutoLevel).to.equal(
|
|
0,
|
|
'firstAutoLevel capped to 0',
|
|
);
|
|
});
|
|
|
|
it('ignores starting level with config.audioPreference.channels when not within bandwidth estimate', function () {
|
|
const { levels, audioTracks } = getMultiChannelAudioLevels();
|
|
(hls as any).levelController._levels = levels;
|
|
(hls as any).audioTrackController.tracks = audioTracks;
|
|
hls.config.audioPreference = {
|
|
channels: '6',
|
|
};
|
|
const bwe = hls.bandwidthEstimate;
|
|
expect(bwe).to.equal(5e5);
|
|
const firstAutoLevel = abrController.firstAutoLevel;
|
|
const level = hls.levels[firstAutoLevel];
|
|
expect(level.bitrate).to.be.lessThanOrEqual(bwe);
|
|
expect(level.codecSet).to.equal('avc1,mp4a');
|
|
expect(level.height).to.equal(1080);
|
|
expect(firstAutoLevel).to.equal(5);
|
|
});
|
|
|
|
it('returns starting level with config.audioPreference.channels when within bandwidth estimate', function () {
|
|
const { levels, audioTracks } = getMultiChannelAudioLevels();
|
|
(hls as any).levelController._levels = levels;
|
|
(hls as any).audioTrackController.tracks = audioTracks;
|
|
hls.config.audioPreference = {
|
|
channels: '6',
|
|
};
|
|
hls.bandwidthEstimate = 501000;
|
|
const firstAutoLevel = abrController.firstAutoLevel;
|
|
const level = hls.levels[firstAutoLevel];
|
|
expect(level.codecSet).to.equal('hevc,ac-3');
|
|
expect(level.height).to.equal(720);
|
|
expect(firstAutoLevel).to.equal(3);
|
|
});
|
|
|
|
it('returns starting level with config.audioPreference.audioCodec when within bandwidth estimate', function () {
|
|
const { levels, audioTracks } = getMultiChannelAudioLevels();
|
|
(hls as any).levelController._levels = levels;
|
|
(hls as any).audioTrackController.tracks = audioTracks;
|
|
hls.config.audioPreference = {
|
|
audioCodec: 'ec-3',
|
|
};
|
|
hls.bandwidthEstimate = 511000;
|
|
const firstAutoLevel = abrController.firstAutoLevel;
|
|
const level = hls.levels[firstAutoLevel];
|
|
expect(level.codecSet).to.equal('hevc,ec-3');
|
|
expect(level.height).to.equal(720);
|
|
expect(firstAutoLevel).to.equal(4);
|
|
});
|
|
|
|
it('returns hls.firstLevel when match cannot be found', function () {
|
|
(hls as any).levelController._levels = getSimpleLevels();
|
|
expect(hls.minAutoLevel).to.equal(0);
|
|
expect(hls.maxAutoLevel).to.equal(hls.levels.length - 1);
|
|
expect(hls.firstLevel).to.equal(0);
|
|
|
|
hls.firstLevel = 3;
|
|
abrController.resetEstimator(hls.levels[hls.firstLevel].bitrate);
|
|
expect(hls.firstLevel).to.equal(3);
|
|
expect(abrController.firstAutoLevel).to.equal(3);
|
|
|
|
abrController.resetEstimator(999999999);
|
|
expect(abrController.firstAutoLevel).to.equal(5);
|
|
|
|
abrController.resetEstimator(1);
|
|
expect(abrController.firstAutoLevel).to.equal(3);
|
|
});
|
|
});
|
|
|
|
describe('nextAutoLevel getter', function () {
|
|
it('returns higher level index with sufficient buffer', function () {
|
|
setForwardBufferlength(sandbox, hls, 8);
|
|
(hls as any).levelController._levels = getSimpleLevels();
|
|
const requiredBitrateForLevel2 = 460560 / 0.7 + 1;
|
|
loadAndBufferFragment(
|
|
abrController,
|
|
0,
|
|
requiredBitrateForLevel2 / 8,
|
|
1000,
|
|
);
|
|
expect(abrController.nextAutoLevel).to.equal(2);
|
|
});
|
|
});
|
|
|
|
describe('nextAutoLevel setter and forcedAutoLevel getter', function () {
|
|
it('forcedAutoLevel returns value set by nextAutoLevel setter until nextAutoLevel can use estimate and find a candidate', function () {
|
|
(hls as any).levelController._levels = getSimpleLevels();
|
|
expect(abrController.forcedAutoLevel).to.equal(-1);
|
|
expect(abrController.nextAutoLevel).to.equal(2);
|
|
|
|
abrController.nextAutoLevel = 1;
|
|
expect(abrController.forcedAutoLevel).to.equal(1);
|
|
expect(abrController.nextAutoLevel).to.equal(1);
|
|
|
|
loadAndBufferFragment(abrController, 1, 5e6, 1000);
|
|
expect(abrController.forcedAutoLevel).to.equal(-1);
|
|
expect(abrController.nextAutoLevel).to.equal(5);
|
|
});
|
|
});
|
|
});
|
|
|
|
function loadAndBufferFragment(
|
|
abrController: AbrController,
|
|
levelIndex: number,
|
|
sizeInBytes: number,
|
|
timeToLoad: number,
|
|
timeToFirstByte: number = 0,
|
|
timeToParse: number = 0,
|
|
) {
|
|
const frag = new Fragment(PlaylistLevelType.MAIN, '');
|
|
frag.level = levelIndex;
|
|
frag.stats = new LoadStats();
|
|
frag.stats.loaded = sizeInBytes;
|
|
frag.stats.loading = {
|
|
first: 0,
|
|
start: timeToFirstByte,
|
|
end: timeToLoad + timeToFirstByte,
|
|
};
|
|
frag.stats.parsing = {
|
|
start: frag.stats.loading.end,
|
|
end: frag.stats.loading.end + timeToParse,
|
|
};
|
|
((abrController as any).onFragLoaded as HlsListeners[Events.FRAG_LOADED])(
|
|
Events.FRAG_LOADED,
|
|
{
|
|
frag,
|
|
part: null,
|
|
payload: new ArrayBuffer(0),
|
|
networkDetails: null,
|
|
},
|
|
);
|
|
((abrController as any).onFragBuffered as HlsListeners[Events.FRAG_BUFFERED])(
|
|
Events.FRAG_BUFFERED,
|
|
{
|
|
frag,
|
|
part: null,
|
|
stats: frag.stats,
|
|
id: 'video',
|
|
},
|
|
);
|
|
}
|
|
|
|
function setForwardBufferlength(
|
|
sandbox: sinon.SinonSandbox,
|
|
hls: Hls,
|
|
length: number,
|
|
) {
|
|
sandbox
|
|
.stub((hls as any).streamController, 'getMainFwdBufferInfo')
|
|
.returns({ len: length, start: 0, end: length, nextStart: undefined });
|
|
}
|
|
|
|
function getSimpleLevels(): Level[] {
|
|
const parsedLevels: LevelParsed[] = [
|
|
{
|
|
bitrate: 105000,
|
|
name: '144',
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
},
|
|
{
|
|
bitrate: 246440,
|
|
name: '240',
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
},
|
|
{
|
|
bitrate: 460560,
|
|
name: '380',
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
},
|
|
{
|
|
bitrate: 836280,
|
|
name: '480',
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
},
|
|
{
|
|
bitrate: 2149280,
|
|
name: '720',
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
},
|
|
{
|
|
bitrate: 6221600,
|
|
name: '1080',
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
},
|
|
];
|
|
return parsedLevels.map((parsedLevel) => new Level(parsedLevel));
|
|
}
|
|
|
|
function getMultiCodecLevels(): Level[] {
|
|
const parsedLevels: LevelParsed[] = [
|
|
{
|
|
bitrate: 336280,
|
|
name: '480',
|
|
width: 852,
|
|
height: 480,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
videoCodec: 'avc1',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 300000,
|
|
name: '480',
|
|
width: 852,
|
|
height: 480,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 414928,
|
|
name: '720',
|
|
width: 1280,
|
|
height: 720,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
videoCodec: 'avc1',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 480928,
|
|
name: '720',
|
|
width: 1280,
|
|
height: 720,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 480000,
|
|
name: '1080',
|
|
width: 1920,
|
|
height: 1080,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
videoCodec: 'avc1',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 522160,
|
|
name: '1080',
|
|
width: 1920,
|
|
height: 1080,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
videoCodec: 'avc1',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 500060,
|
|
name: '1080',
|
|
width: 1920,
|
|
height: 1080,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({}),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
];
|
|
return parsedLevels.map((parsedLevel) => new Level(parsedLevel));
|
|
}
|
|
|
|
function getMultiChannelAudioLevels(): {
|
|
levels: Level[];
|
|
audioTracks: MediaPlaylist[];
|
|
} {
|
|
const parsedLevels: LevelParsed[] = [
|
|
{
|
|
bitrate: 336280,
|
|
name: '480',
|
|
width: 852,
|
|
height: 480,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'mp4a' }),
|
|
url: '',
|
|
videoCodec: 'avc1',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 414928,
|
|
name: '720',
|
|
width: 1280,
|
|
height: 720,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'mp4a' }),
|
|
url: '',
|
|
videoCodec: 'avc1',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 480928,
|
|
name: '720',
|
|
width: 1280,
|
|
height: 720,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'mp4a' }),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 500928,
|
|
name: '720',
|
|
width: 1280,
|
|
height: 720,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'ac-3' }),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'ac-3',
|
|
},
|
|
{
|
|
bitrate: 510928,
|
|
name: '720',
|
|
width: 1280,
|
|
height: 720,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'ec-3' }),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'ec-3',
|
|
},
|
|
{
|
|
bitrate: 480000,
|
|
name: '1080',
|
|
width: 1920,
|
|
height: 1080,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'mp4a' }),
|
|
url: '',
|
|
videoCodec: 'avc1',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 500060,
|
|
name: '1080',
|
|
width: 1920,
|
|
height: 1080,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'mp4a' }),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'mp4a',
|
|
},
|
|
{
|
|
bitrate: 510060,
|
|
name: '1080',
|
|
width: 1920,
|
|
height: 1080,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'ac-3' }),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'ac-3',
|
|
},
|
|
{
|
|
bitrate: 520060,
|
|
name: '1080',
|
|
width: 1920,
|
|
height: 1080,
|
|
details: levelDetailsWithDuration(10),
|
|
attrs: new AttrList({ AUDIO: 'ec-3' }),
|
|
url: '',
|
|
videoCodec: 'hevc',
|
|
audioCodec: 'ec-3',
|
|
},
|
|
];
|
|
const levels = parsedLevels.map((parsedLevel) => {
|
|
const level = new Level(parsedLevel);
|
|
return level;
|
|
});
|
|
const audioTracks: MediaPlaylist[] = [
|
|
{
|
|
attrs: new AttrList({}) as MediaAttributes,
|
|
autoselect: true,
|
|
bitrate: 0,
|
|
default: true,
|
|
forced: false,
|
|
channels: '2',
|
|
groupId: 'mp4a',
|
|
name: 'English',
|
|
type: 'AUDIO',
|
|
id: 0,
|
|
url: 'data://',
|
|
},
|
|
{
|
|
attrs: new AttrList({}) as MediaAttributes,
|
|
autoselect: true,
|
|
bitrate: 0,
|
|
default: true,
|
|
forced: false,
|
|
channels: '6',
|
|
groupId: 'ac-3',
|
|
name: 'English',
|
|
type: 'AUDIO',
|
|
id: 0,
|
|
url: 'data://',
|
|
},
|
|
{
|
|
attrs: new AttrList({}) as MediaAttributes,
|
|
autoselect: true,
|
|
bitrate: 0,
|
|
default: true,
|
|
forced: false,
|
|
channels: '16/JOC',
|
|
groupId: 'ec-3',
|
|
name: 'English',
|
|
type: 'AUDIO',
|
|
id: 0,
|
|
url: 'data://',
|
|
},
|
|
];
|
|
return {
|
|
levels,
|
|
audioTracks,
|
|
};
|
|
}
|