mirror of
https://github.com/video-dev/hls.js.git
synced 2026-05-17 13:30:38 +00:00
1201 lines
40 KiB
TypeScript
1201 lines
40 KiB
TypeScript
import { expect, use } from 'chai';
|
|
import { fakeServer } from 'nise';
|
|
import sinon from 'sinon';
|
|
import sinonChai from 'sinon-chai';
|
|
import { multivariantPlaylistWithRedundantFallbacks } from './level-controller';
|
|
import { ErrorDetails, ErrorTypes } from '../../../src/errors';
|
|
import { Events } from '../../../src/events';
|
|
import Hls from '../../../src/hls';
|
|
import type {
|
|
ErrorData,
|
|
FragLoadedData,
|
|
LevelSwitchingData,
|
|
} from '../../../src/types/events';
|
|
|
|
use(sinonChai);
|
|
|
|
describe('ErrorController Integration Tests', function () {
|
|
let server: sinon.SinonFakeServer;
|
|
let timers: sinon.SinonFakeTimers;
|
|
let hls: Hls;
|
|
|
|
beforeEach(function () {
|
|
server = fakeServer.create();
|
|
setupMockServerResponses(server);
|
|
timers = sinon.useFakeTimers({ shouldClearNativeTimers: true } as any);
|
|
|
|
hls = new Hls({
|
|
// Enable debug to catch callback errors and enable logging in these tests:
|
|
// debug: true,
|
|
startFragPrefetch: true,
|
|
enableWorker: false,
|
|
testBandwidth: false,
|
|
});
|
|
sinon.spy(hls, 'stopLoad');
|
|
sinon.spy(hls, 'trigger');
|
|
});
|
|
|
|
afterEach(function () {
|
|
server.restore();
|
|
timers.restore();
|
|
hls.destroy();
|
|
});
|
|
|
|
describe('Multivariant Playlist Error Handling', function () {
|
|
it('Manifest Parsing Errors are fatal and stop all network operations', function () {
|
|
hls.loadSource('noEXTM3U.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.MANIFEST_LOADED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Manifest Loaded should not be triggered when manifest parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.MANIFEST_PARSING_ERROR,
|
|
'no EXTM3U delimiter',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Manifest Parsing Errors (no variants) are fatal and stop all network operations', function () {
|
|
hls.loadSource('noLevels.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.MANIFEST_LOADED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Manifest Loaded should not be triggered when manifest parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.MANIFEST_PARSING_ERROR,
|
|
'no levels found in manifest',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Manifest Parsing Errors (Variable Substitution) are fatal and stop all network operations', function () {
|
|
hls.loadSource('varSubErrorMultivariantPlaylist.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.MANIFEST_LOADED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Manifest Loaded should not be triggered when manifest parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.MANIFEST_PARSING_ERROR,
|
|
'Missing preceding EXT-X-DEFINE tag for Variable Reference: "foobar"',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Manifest Incompatible Codecs Errors are fatal and stop all network operations', function () {
|
|
hls.loadSource('noCompatCodecs.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.MANIFEST_PARSED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Manifest Parsed should not be triggered when manifest parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
|
|
'no level with compatible codecs found in manifest (one or more CODECS in variant not supported: ["avc9.000000,mp5a.40.2,av99.000000"])',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Manifest HTTP 4XX Load Errors are fatal and stop all network operations', function () {
|
|
server.respondWith('http400.m3u8', [400, {}, ``]);
|
|
hls.loadSource('http400.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.MANIFEST_PARSED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Manifest Parsed should not be triggered when manifest parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.MANIFEST_LOAD_ERROR,
|
|
'A network error (status 400) occurred while loading manifest',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Manifest HTTP status 501 and >= 505 Errors fail silently until exhausting all retries then are fatal', function () {
|
|
server.respondWith('http500.m3u8', [501, {}, ``]);
|
|
hls.loadSource('http500.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.MANIFEST_PARSED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Manifest Parsed should not be triggered when manifest parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
timers.tick(1000);
|
|
server.respond();
|
|
})
|
|
.then((data) => {
|
|
expect(server.requests.length).to.equal(2);
|
|
expect(server.requests[0])
|
|
.to.have.property('url')
|
|
.which.equals('http500.m3u8');
|
|
expect(server.requests[0])
|
|
.to.have.property('status')
|
|
.which.equals(501);
|
|
expect(server.requests[1])
|
|
.to.have.property('url')
|
|
.which.equals('http500.m3u8');
|
|
expect(server.requests[1])
|
|
.to.have.property('status')
|
|
.which.equals(501);
|
|
return data;
|
|
})
|
|
.then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.MANIFEST_LOAD_ERROR,
|
|
'A network error (status 501) occurred while loading manifest',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Manifest Load Timeout Errors are fatal and stop all network operations', function () {
|
|
hls.loadSource('timeout.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.MANIFEST_PARSED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Manifest Parsed should not be triggered when manifest parsing fails',
|
|
),
|
|
),
|
|
);
|
|
// tick 3 times to trigger 2 retries and then an error
|
|
timers.tick(hls.config.manifestLoadPolicy.default.maxLoadTimeMs + 1);
|
|
timers.tick(hls.config.manifestLoadPolicy.default.maxLoadTimeMs + 1);
|
|
timers.tick(hls.config.manifestLoadPolicy.default.maxLoadTimeMs);
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.MANIFEST_LOAD_TIMEOUT,
|
|
'A network timeout occurred while loading manifest',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Variant Media Playlist (no Multivariant Loaded) Error Handling', function () {
|
|
it('Level Parsing Errors (Variable Substitution) are escalated to fatal when no switch options are present', function () {
|
|
hls.loadSource('varSubErrorMediaPlaylist.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.LEVEL_LOADED, () =>
|
|
reject(
|
|
new Error(
|
|
'Level Loaded should not be triggered when playlist parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.LEVEL_PARSING_ERROR,
|
|
'Missing preceding EXT-X-DEFINE tag for Variable Reference: "foobar"',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Level Parsing Errors (Missing Target Duration) are escalated to fatal when no switch options are present', function () {
|
|
hls.loadSource('noTargetDuration.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.LEVEL_LOADED, () =>
|
|
reject(
|
|
new Error(
|
|
'Level Loaded should not be triggered when playlist parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.LEVEL_PARSING_ERROR,
|
|
'Missing Target Duration',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Level Empty Errors (No Segments) are escalated to fatal when no switch options are present and Playlist is VOD', function () {
|
|
hls.loadSource('noSegmentsVod.m3u8');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
Promise.resolve().then(() => resolve(data));
|
|
});
|
|
hls.on(Events.LEVEL_LOADED, () =>
|
|
reject(
|
|
new Error(
|
|
'Level Loaded should not be triggered when playlist parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.LEVEL_EMPTY_ERROR,
|
|
'No Segments found in Playlist',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Level Empty Errors (No Segments) are not fatal when Playlist with no switch options is Live', function () {
|
|
hls.loadSource('noSegmentsLive.m3u8');
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
hls.on(Events.LEVEL_LOADED, () =>
|
|
reject(
|
|
new Error(
|
|
'Level Loaded should not be triggered when playlist parsing fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.LEVEL_EMPTY_ERROR);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.equal(
|
|
'No Segments found in Playlist',
|
|
data.error.message,
|
|
);
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
expect(hls.trigger).to.not.have.been.calledWith(Events.LEVEL_LOADED);
|
|
server.respondWith(
|
|
'noSegmentsLive.m3u8',
|
|
testResponses['oneSegmentLive.m3u8'],
|
|
);
|
|
timers.tick(6000);
|
|
server.respond();
|
|
expect(hls.trigger).to.have.been.calledWith(Events.LEVEL_LOADED);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Multivariant Media Playlist Error Handling', function () {
|
|
it('Level Parsing Errors (Missing Target Duration) are not fatal when switch options are present', function () {
|
|
hls.loadSource('multivariantPlaylist.m3u8');
|
|
let errorIndex = -1;
|
|
hls.once(Events.LEVEL_LOADING, (event, data) => {
|
|
errorIndex = data.level;
|
|
server.respondWith(data.url, testResponses['noTargetDuration.m3u8']);
|
|
});
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
server.respond();
|
|
})
|
|
.then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.LEVEL_PARSING_ERROR);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.equal(
|
|
'Missing Target Duration',
|
|
data.error.message,
|
|
);
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
timers.tick(100);
|
|
return Promise.resolve();
|
|
})
|
|
.then(() => {
|
|
expect(hls.trigger).to.have.been.calledWith(Events.LEVEL_LOADED);
|
|
expect(hls.currentLevel).to.not.equal(
|
|
errorIndex,
|
|
'Should not be on errored level',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('Level HTTP 4XX Load Errors are not fatal when switch options are present', function () {
|
|
hls.loadSource('multivariantPlaylist.m3u8');
|
|
let errorIndex = -1;
|
|
hls.once(Events.LEVEL_LOADING, (event, data) => {
|
|
errorIndex = data.level;
|
|
server.respondWith(data.url, [400, {}, '']);
|
|
});
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
server.respond();
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.LEVEL_LOAD_ERROR);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.equal(
|
|
'A network error (status 400) occurred while loading level: 2 id: 0',
|
|
data.error.message,
|
|
);
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
expect(hls.trigger).to.have.been.calledWith(Events.LEVEL_LOADED);
|
|
expect(hls.currentLevel).to.not.equal(
|
|
errorIndex,
|
|
'Should not be on errored level',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('Level Load Timeout Errors are not fatal when switch options are present', function () {
|
|
hls.loadSource('multivariantPlaylist.m3u8');
|
|
let errorIndex = -1;
|
|
hls.once(Events.LEVEL_LOADING, (event, data) => {
|
|
errorIndex = data.level;
|
|
});
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
server.respond();
|
|
timers.tick(20000);
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.LEVEL_LOAD_TIMEOUT);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.equal(
|
|
'A network timeout occurred while loading level: 2 id: 0',
|
|
data.error.message,
|
|
);
|
|
server.respond();
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
expect(hls.trigger).to.have.been.calledWith(Events.LEVEL_LOADED);
|
|
expect(hls.currentLevel).to.not.equal(
|
|
errorIndex,
|
|
'Should not be on errored level',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Live Playlist Unchanged Error Handling', function () {
|
|
it('PLAYLIST_UNCHANGED_ERROR is triggered after max unchanged reloads and switches level', function () {
|
|
(hls.config as any).liveMaxUnchangedPlaylistRefresh = 2;
|
|
|
|
server.respondWith('liveMultivariant.m3u8', [
|
|
200,
|
|
{},
|
|
`#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=1280x720
|
|
live-mid.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=100000,RESOLUTION=480x270
|
|
live-low.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=300000,RESOLUTION=1920x1080
|
|
live-high.m3u8`,
|
|
]);
|
|
|
|
// Live playlist that won't change (same EXT-X-MEDIA-SEQUENCE)
|
|
const livePlaylist = `#EXTM3U
|
|
#EXT-X-VERSION:3
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXT-X-MEDIA-SEQUENCE:100
|
|
#EXTINF:6,
|
|
segment100.ts
|
|
#EXTINF:6,
|
|
segment101.ts`;
|
|
|
|
server.respondWith(/live-.*\.m3u8/, [200, {}, livePlaylist]);
|
|
|
|
hls.loadSource('liveMultivariant.m3u8');
|
|
|
|
return new Promise<ErrorData>((resolve) => {
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
if (data.details === ErrorDetails.PLAYLIST_UNCHANGED_ERROR) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
hls.on(Events.LEVEL_LOADED, () => {
|
|
// Advance time to trigger playlist refresh (targetduration * 1000)
|
|
timers.tick(6000);
|
|
});
|
|
server.respond();
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.PLAYLIST_UNCHANGED_ERROR);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.include(
|
|
'hit max allowed unchanged reloads',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('PLAYLIST_UNCHANGED_ERROR is not fatal when no alternate levels are available', function () {
|
|
(hls.config as any).liveMaxUnchangedPlaylistRefresh = 2;
|
|
|
|
server.respondWith('singleLevelMultivariant.m3u8', [
|
|
200,
|
|
{},
|
|
`#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=1280x720
|
|
single-live.m3u8`,
|
|
]);
|
|
|
|
// Live playlist that won't change (same EXT-X-MEDIA-SEQUENCE)
|
|
const livePlaylist = `#EXTM3U
|
|
#EXT-X-VERSION:3
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXT-X-MEDIA-SEQUENCE:100
|
|
#EXTINF:6,
|
|
segment100.ts
|
|
#EXTINF:6,
|
|
segment101.ts`;
|
|
|
|
server.respondWith(/single-live\.m3u8/, [200, {}, livePlaylist]);
|
|
|
|
hls.loadSource('singleLevelMultivariant.m3u8');
|
|
|
|
return new Promise<ErrorData>((resolve) => {
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
if (data.details === ErrorDetails.PLAYLIST_UNCHANGED_ERROR) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
hls.on(Events.LEVEL_LOADED, () => {
|
|
// Advance time to trigger playlist refresh
|
|
timers.tick(6000);
|
|
});
|
|
server.respond();
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.PLAYLIST_UNCHANGED_ERROR);
|
|
expect(data.fatal).to.equal(
|
|
false,
|
|
'Error should not be fatal with no alternates',
|
|
);
|
|
expect(data.error.message).to.include(
|
|
'hit max allowed unchanged reloads',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('PLAYLIST_UNCHANGED_ERROR for audio track triggers level switch', function () {
|
|
(hls.config as any).liveMaxUnchangedPlaylistRefresh = 2;
|
|
|
|
server.respondWith('liveWithAudio.m3u8', [
|
|
200,
|
|
{},
|
|
`#EXTM3U
|
|
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-en",NAME="English",URI="audio-en.m3u8"
|
|
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-es",NAME="Spanish",URI="audio-es.m3u8"
|
|
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=1280x720,AUDIO="audio-en"
|
|
video-mid.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=100000,RESOLUTION=480x270,AUDIO="audio-es"
|
|
video-low.m3u8`,
|
|
]);
|
|
|
|
// Live video playlist that advances normally
|
|
const liveVideoPlaylist = `#EXTM3U
|
|
#EXT-X-VERSION:3
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXT-X-MEDIA-SEQUENCE:100
|
|
#EXTINF:6,
|
|
video100.ts
|
|
#EXTINF:6,
|
|
video101.ts`;
|
|
|
|
// Live playlist that won't change (same EXT-X-MEDIA-SEQUENCE)
|
|
const liveAudioPlaylist = `#EXTM3U
|
|
#EXT-X-VERSION:3
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXT-X-MEDIA-SEQUENCE:100
|
|
#EXTINF:6,
|
|
audio100.ts
|
|
#EXTINF:6,
|
|
audio101.ts`;
|
|
|
|
server.respondWith(/video-.*\.m3u8/, [200, {}, liveVideoPlaylist]);
|
|
server.respondWith(/audio-.*\.m3u8/, [200, {}, liveAudioPlaylist]);
|
|
|
|
hls.loadSource('liveWithAudio.m3u8');
|
|
|
|
return new Promise<ErrorData>((resolve) => {
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(
|
|
Events.AUDIO_TRACK_LOADING,
|
|
loadingEventCallback(server, timers),
|
|
);
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
if (data.details === ErrorDetails.PLAYLIST_UNCHANGED_ERROR) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
hls.on(Events.AUDIO_TRACK_LOADED, () => {
|
|
// Advance time to trigger audio playlist refresh
|
|
timers.tick(6000);
|
|
});
|
|
server.respond();
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.PLAYLIST_UNCHANGED_ERROR);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.include(
|
|
'hit max allowed unchanged reloads',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Segment Error Handling', function () {
|
|
it('Fragment HTTP Load Errors retry fragLoadPolicy `errorRetry.maxNumRetry` times before switching down and continues until no lower levels are available', function () {
|
|
server.respondWith('multivariantPlaylist.m3u8/segment.mp4', [
|
|
500,
|
|
{},
|
|
new ArrayBuffer(0),
|
|
]);
|
|
hls.loadSource('multivariantPlaylist.m3u8');
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.FRAG_LOADING, loadingEventCallback(server, timers));
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
if (data.fatal) {
|
|
resolve(data);
|
|
} else {
|
|
timers.tick(
|
|
hls.config.fragLoadPolicy.default.errorRetry!.maxRetryDelayMs,
|
|
);
|
|
}
|
|
});
|
|
hls.on(Events.FRAG_LOADED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Frag Loaded should not be triggered when frag loading fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then((errorData: ErrorData) => {
|
|
expect(server.requests).to.have.lengthOf(13);
|
|
const finalAssertion = expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.FRAG_LOAD_ERROR,
|
|
'HTTP Error 500 Internal Server Error',
|
|
);
|
|
finalAssertion(errorData);
|
|
});
|
|
});
|
|
|
|
it('Fragment Timout Errors retry within a tick fragLoadPolicy `timeoutRetry.maxNumRetry` times before switching down and continues no lower levels are available', function () {
|
|
server.respondWith('multivariantPlaylist.m3u8/segment.mp4', [
|
|
400,
|
|
{},
|
|
new ArrayBuffer(0),
|
|
]);
|
|
hls.loadSource('multivariantPlaylist.m3u8');
|
|
hls.on(Events.LEVEL_LOADING, (event, data) => {
|
|
server.respond();
|
|
});
|
|
hls.on(Events.FRAG_LOADING, (event, data) => {
|
|
timers.tick(hls.config.fragLoadPolicy.default.maxTimeToFirstByteMs);
|
|
});
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
if (data.fatal) {
|
|
resolve(data);
|
|
} else {
|
|
timers.tick(100);
|
|
}
|
|
});
|
|
hls.on(Events.FRAG_LOADED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Frag Loaded should not be triggered when frag loading fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then((errorData: ErrorData) => {
|
|
expect(server.requests).to.have.lengthOf(11);
|
|
const finalAssertion = expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.FRAG_LOAD_TIMEOUT,
|
|
'Timeout after 10000ms',
|
|
);
|
|
finalAssertion(errorData);
|
|
});
|
|
});
|
|
|
|
it('Init segment decrypt errors are fatal with no alternates after retries', function () {
|
|
server.respondWith(
|
|
'aes-128-init-segment.m3u8',
|
|
`#EXTM3U
|
|
#EXT-X-VERSION:1
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXT-X-KEY:METHOD=AES-128,URI="bad.key",IV=0x0000000000
|
|
#EXT-X-MAP:URI="init.mp4"
|
|
#EXTINF:6
|
|
segment.mp4
|
|
#EXT-X-ENDLIST`,
|
|
);
|
|
server.respondWith('aes-128-init-segment.m3u8/bad.key', [
|
|
200,
|
|
{},
|
|
new ArrayBuffer(16),
|
|
]);
|
|
server.respondWith('aes-128-init-segment.m3u8/init.mp4', [
|
|
200,
|
|
{},
|
|
new ArrayBuffer(1024),
|
|
]);
|
|
server.respondWith('aes-128-init-segment.m3u8/segment.mp4', [
|
|
200,
|
|
{},
|
|
new ArrayBuffer(1024),
|
|
]);
|
|
hls.config.fragLoadPolicy.default.errorRetry!.maxNumRetry = 1;
|
|
hls.loadSource('aes-128-init-segment.m3u8');
|
|
hls.on(Events.KEY_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.FRAG_LOADING, loadingEventCallback(server, timers));
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
if (data.fatal) {
|
|
resolve(data);
|
|
} else {
|
|
timers.tick(2000);
|
|
}
|
|
});
|
|
hls.on(Events.FRAG_DECRYPTED, (event, data) =>
|
|
reject(
|
|
new Error(
|
|
'Frag Decrypted should not be triggered when frag decryption fails',
|
|
),
|
|
),
|
|
);
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.FRAG_DECRYPT_ERROR,
|
|
'Offset is outside the bounds of the DataView',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Media Error Handling', function () {
|
|
it('treats MEDIA_SOURCE_REQUIRES_RESET as recoverable and calls recoverMediaError', function () {
|
|
const recoverSpy = sinon.spy(hls, 'recoverMediaError');
|
|
const data: any = {
|
|
type: ErrorTypes.MEDIA_ERROR,
|
|
details: ErrorDetails.MEDIA_SOURCE_REQUIRES_RESET,
|
|
fatal: false,
|
|
error: new Error(
|
|
'MediaSource requires reset while media is still attached',
|
|
),
|
|
};
|
|
hls.trigger(Events.ERROR, data);
|
|
expect(recoverSpy).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('Transmuxer Error Handling', function () {
|
|
it('Fragment parsing errors are fatal with no alternates after retries', function () {
|
|
server.respondWith('oneSegmentVod-mp2ts.m3u8/segment.ts', [
|
|
200,
|
|
{},
|
|
new ArrayBuffer(188 * 5),
|
|
]);
|
|
hls.config.fragLoadPolicy.default.errorRetry!.maxNumRetry = 2;
|
|
hls.loadSource('oneSegmentVod-mp2ts.m3u8');
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.FRAG_LOADING, loadingEventCallback(server, timers));
|
|
let errorCount = 0;
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
errorCount++;
|
|
if (errorCount === 3 && data.fatal) {
|
|
resolve(data);
|
|
} else if (data.fatal) {
|
|
reject(
|
|
new Error(
|
|
`Error fatal before retries exhausted: "${data.error.message}"`,
|
|
),
|
|
);
|
|
} else {
|
|
timers.tick(8000);
|
|
}
|
|
});
|
|
server.respond();
|
|
}).then(
|
|
expectFatalErrorEventToStopPlayer(
|
|
hls,
|
|
ErrorDetails.FRAG_PARSING_ERROR,
|
|
'Failed to find demuxer by probing fragment data',
|
|
),
|
|
);
|
|
});
|
|
|
|
it('Remux Allocation Errors are not fatal when switch options are present', function () {
|
|
hls.loadSource('multivariantPlaylist.m3u8');
|
|
let errorIndex = -1;
|
|
hls.on(Events.LEVEL_LOADING, (event, data) => {
|
|
server.respond();
|
|
});
|
|
hls.once(Events.LEVEL_LOADED, (event, data) => {
|
|
hls.trigger(Events.ERROR, {
|
|
type: ErrorTypes.MUX_ERROR,
|
|
details: ErrorDetails.REMUX_ALLOC_ERROR,
|
|
fatal: false,
|
|
bytes: 999999999999,
|
|
error: new Error('OOM Error'),
|
|
reason: `fail allocating video mdat ${999999999999}`,
|
|
});
|
|
errorIndex = data.level;
|
|
});
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
server.respond();
|
|
timers.tick(5000);
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(ErrorDetails.REMUX_ALLOC_ERROR);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.equal('OOM Error');
|
|
server.respond();
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
expect(hls.trigger).to.have.been.calledWith(Events.LEVEL_LOADED);
|
|
expect(hls.currentLevel).to.not.equal(
|
|
errorIndex,
|
|
'Should not be on errored level',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Key-System Error Handling', function () {
|
|
it('handles Key-System Output Restricted Errors by setting hls.maxHdcpLevel', function () {
|
|
hls.loadSource('multivariantPlaylist-HDCP-LEVEL.m3u8');
|
|
hls.startLevel = 2;
|
|
expect(hls.maxHdcpLevel).to.equal(null);
|
|
let errorIndex = -1;
|
|
hls.on(Events.LEVEL_LOADING, (event, data) => {
|
|
server.respond();
|
|
});
|
|
hls.once(Events.LEVEL_LOADED, (event, data) => {
|
|
hls.trigger(Events.ERROR, {
|
|
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
details: ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED,
|
|
fatal: false,
|
|
error: new Error('HDCP level output restricted'),
|
|
});
|
|
errorIndex = data.level;
|
|
});
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.ERROR, (event, data) => resolve(data));
|
|
server.respond();
|
|
timers.tick(5000);
|
|
}).then((data: ErrorData) => {
|
|
expect(data.details).to.equal(
|
|
ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED,
|
|
);
|
|
expect(data.fatal).to.equal(false, 'Error should not be fatal');
|
|
expect(data.error.message).to.equal('HDCP level output restricted');
|
|
expect(hls.stopLoad).to.have.been.calledOnce;
|
|
expect(hls.trigger).to.have.been.calledWith(Events.LEVEL_LOADED);
|
|
expect(hls.maxHdcpLevel).to.equal('TYPE-0');
|
|
expect(hls.currentLevel).to.not.equal(
|
|
errorIndex,
|
|
'Should not be on errored level',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Redundant Stream Error Handling', function () {
|
|
it('switches to fallback variants after fragLoadPolicy `errorRetry.maxNumRetry` segment errors in level', function () {
|
|
const errors: ErrorData[] = [];
|
|
// All segments from foo and bar fail, baz succeeds
|
|
const fakeMP2TS = new ArrayBuffer(188 * 3);
|
|
const view = new Uint8Array(fakeMP2TS);
|
|
view[0] = view[188] = view[376] = 0x47;
|
|
server.respondWith(
|
|
/http:\/\/www\.(foo|bar|baz)\.com\/tier.+\.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'].replace(
|
|
'segment.mp4',
|
|
'video-segment.mp4',
|
|
),
|
|
);
|
|
server.respondWith(
|
|
/http:\/\/www\.(foo|bar|baz)\.com\/audio.+\.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'].replace(
|
|
'segment.mp4',
|
|
'audio-segment.mp4',
|
|
),
|
|
);
|
|
server.respondWith(
|
|
/http:\/\/www\.(foo|bar|baz)\.com\/subs.+\.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'].replace(
|
|
'segment.mp4',
|
|
'subs-segment.mp4',
|
|
),
|
|
);
|
|
server.respondWith(
|
|
/http:\/\/www\.(foo|bar)\.com\/(video|audio)-segment.mp4/,
|
|
[500, {}, new ArrayBuffer(0)],
|
|
);
|
|
server.respondWith(/http:\/\/www\.baz\.com\/audio-segment.mp4/, [
|
|
200,
|
|
{},
|
|
fakeMP2TS,
|
|
]);
|
|
server.respondWith(/http:\/\/www\.baz\.com\/subs-segment.mp4/, [
|
|
200,
|
|
{},
|
|
'',
|
|
]);
|
|
server.respondWith(/http:\/\/www\.baz\.com\/.+segment.mp4/, [
|
|
200,
|
|
{},
|
|
fakeMP2TS,
|
|
]);
|
|
hls.config.fragLoadPolicy.default.errorRetry!.maxNumRetry = 1;
|
|
hls.loadSource('multivariantRedundantFallbacks.m3u8');
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.AUDIO_TRACK_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(
|
|
Events.SUBTITLE_TRACK_LOADING,
|
|
loadingEventCallback(server, timers),
|
|
);
|
|
hls.on(Events.FRAG_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
errors.push(data);
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
Promise.resolve().then(() => timers.tick(2000));
|
|
});
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.LEVEL_SWITCHING, (event, data) => {
|
|
if (data.uri.startsWith('http://www.bar.com/')) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
server.respond();
|
|
})
|
|
.then((data: LevelSwitchingData) => {
|
|
expect(
|
|
errors,
|
|
'fragment errors after yeilding to first error event',
|
|
).to.have.lengthOf(2);
|
|
expect(hls.levels[0].uri).to.equal('http://www.bar.com/tier6.m3u8');
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.LEVEL_SWITCHING, (event, data) => {
|
|
if (data.uri.startsWith('http://www.baz.com/')) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
});
|
|
})
|
|
.then((data: LevelSwitchingData) => {
|
|
expect(
|
|
errors,
|
|
'fragment errors after yeilding to second error event',
|
|
).to.have.lengthOf(8);
|
|
expect(hls.levels[0].uri).to.equal('http://www.baz.com/tier6.m3u8');
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.FRAG_LOADED, (event, data) => {
|
|
resolve(data);
|
|
});
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
reject(
|
|
new Error(`Unexpected error after fallback: ${data.error}`),
|
|
);
|
|
});
|
|
});
|
|
})
|
|
.then((data: FragLoadedData) => {
|
|
expect(errors[errors.length - 1].fatal).to.equal(
|
|
false,
|
|
'Error should not be fatal',
|
|
);
|
|
expect(data.frag.url).to.equal(
|
|
'http://www.baz.com/video-segment.mp4',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('switches to fallback variants after media track error', function () {
|
|
const errors: ErrorData[] = [];
|
|
// All segments from foo and bar fail, baz succeeds
|
|
const fakeMP2TS = new ArrayBuffer(188 * 3);
|
|
const view = new Uint8Array(fakeMP2TS);
|
|
view[0] = view[188] = view[376] = 0x47;
|
|
server.respondWith(
|
|
/http:\/\/www\.(foo|bar|baz)\.com\/tier.+\.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'].replace(
|
|
'segment.mp4',
|
|
'video-segment.mp4',
|
|
),
|
|
);
|
|
server.respondWith(
|
|
/http:\/\/www\.(foo|bar|baz)\.com\/audio.+\.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'].replace(
|
|
'segment.mp4',
|
|
'audio-segment.mp4',
|
|
),
|
|
);
|
|
server.respondWith(
|
|
/http:\/\/www\.(foo|bar|baz)\.com\/subs.+\.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'].replace(
|
|
'segment.mp4',
|
|
'subs-segment.mp4',
|
|
),
|
|
);
|
|
server.respondWith(/http:\/\/www\.(foo|bar)\.com\/audio-segment.mp4/, [
|
|
500,
|
|
{},
|
|
new ArrayBuffer(0),
|
|
]);
|
|
server.respondWith(/http:\/\/www\.baz\.com\/video-segment.mp4/, [
|
|
200,
|
|
{},
|
|
fakeMP2TS,
|
|
]);
|
|
server.respondWith(/http:\/\/www\.baz\.com\/subs-segment.mp4/, [
|
|
200,
|
|
{},
|
|
'',
|
|
]);
|
|
server.respondWith(/http:\/\/www\.baz\.com\/.+segment.mp4/, [
|
|
200,
|
|
{},
|
|
fakeMP2TS,
|
|
]);
|
|
hls.config.fragLoadPolicy.default.errorRetry!.maxNumRetry = 1;
|
|
hls.loadSource('multivariantRedundantFallbacks.m3u8');
|
|
hls.on(Events.LEVEL_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.AUDIO_TRACK_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(
|
|
Events.SUBTITLE_TRACK_LOADING,
|
|
loadingEventCallback(server, timers),
|
|
);
|
|
hls.on(Events.FRAG_LOADING, loadingEventCallback(server, timers));
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
errors.push(data);
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
Promise.resolve().then(() => timers.tick(2000));
|
|
});
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.LEVEL_SWITCHING, (event, data) => {
|
|
if (data.uri.startsWith('http://www.bar.com/')) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
server.respond();
|
|
})
|
|
.then((data: LevelSwitchingData) => {
|
|
expect(
|
|
errors,
|
|
'fragment errors after yeilding to first error event',
|
|
).to.have.lengthOf(2);
|
|
expect(hls.levels[0].uri).to.equal('http://www.bar.com/tier6.m3u8');
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.LEVEL_SWITCHING, (event, data) => {
|
|
if (data.uri.startsWith('http://www.baz.com/')) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
});
|
|
})
|
|
.then((data: LevelSwitchingData) => {
|
|
expect(
|
|
errors,
|
|
'fragment errors after yeilding to second error event',
|
|
).to.have.lengthOf(7);
|
|
expect(hls.levels[0].uri).to.equal('http://www.baz.com/tier6.m3u8');
|
|
return new Promise((resolve, reject) => {
|
|
hls.on(Events.FRAG_LOADED, (event, data) => {
|
|
resolve(data);
|
|
});
|
|
hls.on(Events.ERROR, (event, data) => {
|
|
reject(new Error('Unexpected error after fallback'));
|
|
});
|
|
});
|
|
})
|
|
.then((data: FragLoadedData) => {
|
|
expect(errors[errors.length - 1].fatal).to.equal(
|
|
false,
|
|
'Error should not be fatal',
|
|
);
|
|
expect(data.frag.url).to.equal(
|
|
'http://www.baz.com/video-segment.mp4',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
const testResponses = {
|
|
'noEXTM3U.m3u8': '#EXT_NOT_HLS',
|
|
|
|
'noLevels.m3u8': '#EXTM3U',
|
|
|
|
'noCompatCodecs.m3u8': `#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=100000,CODECS="avc9.000000,mp5a.40.2,av99.000000",RESOLUTION=480x270
|
|
noop.m3u8`,
|
|
|
|
'varSubErrorMultivariantPlaylist.m3u8': `#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=100000,RESOLUTION=480x270
|
|
variant{$foobar}.m3u8`,
|
|
|
|
'multivariantPlaylist.m3u8': `#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=1280x720
|
|
mid.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=100000,RESOLUTION=480x270
|
|
low.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=300000,RESOLUTION=1920x1080
|
|
high.m3u8`,
|
|
|
|
'multivariantPlaylist-HDCP-LEVEL.m3u8': `#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=200000,HDCP-LEVEL=TYPE-0,RESOLUTION=1280x720
|
|
mid.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=100000,HDCP-LEVEL=NONE,RESOLUTION=480x270
|
|
low.m3u8
|
|
#EXT-X-STREAM-INF:BANDWIDTH=300000,HDCP-LEVEL=TYPE-1,RESOLUTION=1920x1080
|
|
high.m3u8`,
|
|
|
|
'multivariantRedundantFallbacks.m3u8':
|
|
multivariantPlaylistWithRedundantFallbacks,
|
|
|
|
'varSubErrorMediaPlaylist.m3u8': `#EXTM3U
|
|
#EXT-X-VERSION:10
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXTINF:6
|
|
segment{$foobar}.mp4`,
|
|
|
|
'noTargetDuration.m3u8': `#EXTM3U
|
|
#EXT-X-VERSION:1
|
|
#EXTINF:6
|
|
segment.mp4`,
|
|
|
|
'noSegmentsVod.m3u8': `#EXTM3U
|
|
#EXT-X-VERSION:1
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXT-X-ENDLIST`,
|
|
|
|
'noSegmentsLive.m3u8': `#EXTM3U
|
|
#EXT-X-VERSION:1
|
|
#EXT-X-TARGETDURATION:6`,
|
|
|
|
'oneSegmentLive.m3u8': `#EXTM3U
|
|
#EXT-X-VERSION:1
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXTINF:6
|
|
segment.mp4`,
|
|
|
|
'oneSegmentVod.m3u8': `#EXTM3U
|
|
#EXT-X-VERSION:1
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXTINF:6
|
|
segment.mp4
|
|
#EXT-X-ENDLIST`,
|
|
|
|
'oneSegmentVod-mp2ts.m3u8': `#EXTM3U
|
|
#EXT-X-VERSION:1
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXTINF:6
|
|
segment.ts
|
|
#EXT-X-ENDLIST`,
|
|
};
|
|
|
|
function setupMockServerResponses(server: sinon.SinonFakeServer) {
|
|
Object.keys(testResponses).forEach((requestUrl) => {
|
|
server.respondWith(requestUrl, [200, {}, testResponses[requestUrl]]);
|
|
});
|
|
server.respondWith(
|
|
/multivariantPlaylist.*\/low.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'],
|
|
);
|
|
server.respondWith(
|
|
/multivariantPlaylist.*\/mid.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'],
|
|
);
|
|
server.respondWith(
|
|
/multivariantPlaylist.*\/high.m3u8/,
|
|
testResponses['oneSegmentVod.m3u8'],
|
|
);
|
|
}
|
|
|
|
function loadingEventCallback(server, timers) {
|
|
return (event, data) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
Promise.resolve().then(() => {
|
|
server.respond();
|
|
});
|
|
};
|
|
}
|
|
|
|
function expectFatalErrorEventToStopPlayer(
|
|
hls: Hls,
|
|
withErrorDetails: ErrorDetails,
|
|
withErrorMessage: string,
|
|
) {
|
|
return (data: ErrorData) => {
|
|
expect(data.details).to.equal(withErrorDetails);
|
|
expect(data.fatal).to.equal(true, 'Error should be fatal');
|
|
expect(data.error.message).to.equal(withErrorMessage, data.error.message);
|
|
expectPlayerStopped(hls);
|
|
expect(hls.stopLoad).to.have.been.calledTwice;
|
|
};
|
|
}
|
|
|
|
function expectPlayerStopped(hlsPrivate: any) {
|
|
hlsPrivate.networkControllers.forEach((controller) => {
|
|
// All stream-controllers are stopped
|
|
if ('state' in controller) {
|
|
expect(controller.state, `${controller.constructor.name}.state`).to.equal(
|
|
'STOPPED',
|
|
);
|
|
}
|
|
// All loaders controllers have destroyed their loaders
|
|
if ('loaders' in controller) {
|
|
expect(controller.loaders, `${controller.constructor.name}.loaders`).to.be
|
|
.empty;
|
|
}
|
|
// All playlist-controllers (level-, track-) have stopped loading
|
|
if ('canLoad' in controller) {
|
|
expect(controller.canLoad, `${controller.constructor.name}.canLoad`).to.be
|
|
.false;
|
|
}
|
|
});
|
|
}
|