Files
shaka-player/test/hls/hls_parser_unit.js
T
Michelle Zhuo 1afceadc9c Refactor Closed Captions Support for HLS
Refactor the closed captions support for HLS, using the same approach as
the closed captions support for Dash.
If closed captions are signaled in the manifest, player will create a
text stream for to represent the closed captions, and text engine will
store and append them. We don't need to set 'useEmbeddedText' value for
closed captions any longer.

Issue #1404

Change-Id: I9a5bf4df7e29d6e6982f29fd5e2df07bc78071d7
2018-11-27 20:59:52 +00:00

1738 lines
56 KiB
JavaScript

/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('HlsParser', function() {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const ManifestParser = shaka.test.ManifestParser;
const TextStreamKind = shaka.util.ManifestParserUtils.TextStreamKind;
const Util = shaka.test.Util;
const vttText = [
'WEBVTT\n',
'\n',
'00:03.837 --> 00:07.297\n',
'Hello, world!\n',
].join('');
/** @type {!shaka.test.FakeNetworkingEngine} */
let fakeNetEngine;
/** @type {!shaka.hls.HlsParser} */
let parser;
/** @type {shaka.extern.ManifestParser.PlayerInterface} */
let playerInterface;
/** @type {shaka.extern.ManifestConfiguration} */
let config;
/** @type {!ArrayBuffer} */
let initSegmentData;
/** @type {!ArrayBuffer} */
let segmentData;
/** @type {!ArrayBuffer} */
let selfInitializingSegmentData;
beforeEach(function() {
// TODO: use StreamGenerator?
initSegmentData = new Uint8Array([
0x00, 0x00, 0x00, 0x30, // size (48)
0x6D, 0x6F, 0x6F, 0x76, // type (moov)
0x00, 0x00, 0x00, 0x28, // trak size (40)
0x74, 0x72, 0x61, 0x6B, // type (trak)
0x00, 0x00, 0x00, 0x20, // mdia size (32)
0x6D, 0x64, 0x69, 0x61, // type (mdia)
0x00, 0x00, 0x00, 0x18, // mdhd size (24)
0x6D, 0x64, 0x68, 0x64, // type (mdhd)
0x00, 0x00, 0x00, 0x00, // version and flags
0x00, 0x00, 0x00, 0x00, // creation time (0)
0x00, 0x00, 0x00, 0x00, // modification time (0)
0x00, 0x00, 0x03, 0xe8, // timescale (1000)
]).buffer;
segmentData = new Uint8Array([
0x00, 0x00, 0x00, 0x24, // size (36)
0x6D, 0x6F, 0x6F, 0x66, // type (moof)
0x00, 0x00, 0x00, 0x1C, // traf size (28)
0x74, 0x72, 0x61, 0x66, // type (traf)
0x00, 0x00, 0x00, 0x14, // tfdt size (20)
0x74, 0x66, 0x64, 0x74, // type (tfdt)
0x01, 0x00, 0x00, 0x00, // version and flags
0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime first 4 bytes (0)
0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime last 4 bytes (0)
]).buffer;
// segment starts at 0s.
selfInitializingSegmentData = shaka.util.Uint8ArrayUtils.concat(
new Uint8Array(initSegmentData),
new Uint8Array(segmentData)).buffer;
fakeNetEngine = new shaka.test.FakeNetworkingEngine();
let retry = shaka.net.NetworkingEngine.defaultRetryParameters();
config = {
retryParameters: retry,
availabilityWindowOverride: NaN,
dash: {
customScheme: function(node) { return null; },
clockSyncUri: '',
ignoreDrmInfo: false,
xlinkFailGracefully: false,
defaultPresentationDelay: 10,
ignoreMinBufferTime: false,
},
};
playerInterface = {
filterNewPeriod: function() {},
filterAllPeriods: function() {},
networkingEngine: fakeNetEngine,
onError: fail,
onEvent: fail,
onTimelineRegionAdded: fail,
};
parser = new shaka.hls.HlsParser();
parser.configure(config);
});
/**
* @param {string} master
* @param {string} media
* @param {shaka.extern.Manifest} manifest
* @return {!Promise.<shaka.extern.Manifest>}
*/
async function testHlsParser(master, media, manifest) {
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/audio2', media)
.setResponseText('test:/video', media)
.setResponseText('test:/video2', media)
.setResponseText('test:/main.vtt', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/main.test', segmentData)
.setResponseValue('test:/selfInit.mp4', selfInitializingSegmentData);
let actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
return actual;
}
it('parses manifest attributes', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'CHANNELS="2",URI="audio"\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
'URI="text"\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",LANGUAGE="es",',
'URI="text2"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const textMedia = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.vtt',
].join('');
const manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.language('en')
.bandwidth(200)
.addPartialStream(ContentType.VIDEO)
.anyInitSegment()
.presentationTimeOffset(0)
.mime('video/mp4', 'avc1')
.frameRate(60)
.size(960, 540)
.addPartialStream(ContentType.AUDIO)
.language('en')
.anyInitSegment()
.presentationTimeOffset(0)
.mime('audio/mp4', 'mp4a')
.channelsCount(2)
.addPartialStream(ContentType.TEXT)
.language('en')
.nullInitSegment()
.presentationTimeOffset(0)
.mime('text/vtt', '')
.kind(TextStreamKind.SUBTITLE)
.addPartialStream(ContentType.TEXT)
.language('es')
.nullInitSegment()
.presentationTimeOffset(0)
.mime('text/vtt', '')
.kind(TextStreamKind.SUBTITLE)
.build();
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseText('test:/text', textMedia)
.setResponseText('test:/text2', textMedia)
.setResponseText('test:/main.vtt', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
parser.start('test:/master', playerInterface)
.then(function(actual) { expect(actual).toEqual(manifest); })
.catch(fail).then(done);
});
it('parses video-only variant', async function() {
let master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
let media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.build();
await testHlsParser(master, media, manifest);
});
it('guesses video-only variant by codecs', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1"\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.build();
await testHlsParser(master, media, manifest);
});
it('parses audio-only variant', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="mp4a"\n',
'audio',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.AUDIO)
.mime('audio/mp4', 'mp4a')
.build();
await testHlsParser(master, media, manifest);
});
it('parses audio+video variant', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'CHANNELS="2",URI="audio"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.addPartialStream(ContentType.AUDIO)
.mime('audio/mp4', 'mp4a')
.build();
await testHlsParser(master, media, manifest);
});
it('parses audio+video variant with closed captions', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",CHANNELS="2",',
'URI="audio"\n',
'#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cap1",LANGUAGE="eng",',
'INSTREAM-ID="CC1"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,CLOSED-CAPTIONS="cap1",AUDIO="aud1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const closedCaptions = new Map([['CC1', 'en']]);
const manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.closedCaptions(closedCaptions)
.addPartialStream(ContentType.AUDIO)
.mime('audio/mp4', 'mp4a')
.build();
await testHlsParser(master, media, manifest);
});
it('parses audio+video variant with no closed captions', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",CHANNELS="2",',
'URI="audio"\n',
'#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cap1",LANGUAGE="eng",',
'INSTREAM-ID="CC1"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,CLOSED-CAPTIONS="NONE",AUDIO="aud1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.addPartialStream(ContentType.AUDIO)
.mime('audio/mp4', 'mp4a')
.build();
await testHlsParser(master, media, manifest);
});
it('handles audio tags on audio streams', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="mp4a",AUDIO="aud1"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'URI="audio"\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.AUDIO)
.mime('audio/mp4', 'mp4a')
.build();
await testHlsParser(master, media, manifest);
});
it('sets seek range correctly for non-zero start', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MEDIA-SEQUENCE:131\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
segmentData = new Uint8Array([
0x00, 0x00, 0x00, 0x24, // size (36)
0x6D, 0x6F, 0x6F, 0x66, // type (moof)
0x00, 0x00, 0x00, 0x1C, // traf size (28)
0x74, 0x72, 0x61, 0x66, // type (traf)
0x00, 0x00, 0x00, 0x14, // tfdt size (20)
0x74, 0x66, 0x64, 0x74, // type (tfdt)
0x01, 0x00, 0x00, 0x00, // version and flags
0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime first 4 bytes (0)
0x00, 0x0A, 0x00, 0x00, // baseMediaDecodeTime last 4 bytes (655360)
]).buffer;
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let manifest = await parser.start('test:/master', playerInterface);
let presentationTimeline = manifest.presentationTimeline;
const stream = manifest.periods[0].variants[0].video;
// baseMediaDecodeTime (655360) / timescale (1000)
expect(stream.presentationTimeOffset).toBe(655.36);
const pos = stream.findSegmentPosition(0);
expect(pos).not.toBe(null);
expect(stream.getSegmentReference(pos).startTime).toBe(0);
expect(presentationTimeline.getSeekRangeStart()).toBe(0);
expect(presentationTimeline.getSeekRangeEnd()).toBe(5);
});
it('parses multiplexed variant', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1,mp4a')
.build();
await testHlsParser(master, media, manifest);
});
it('parses multiplexed variant without codecs', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', jasmine.any(String))
.build();
await testHlsParser(master, media, manifest);
});
it('parses audio+video variant without codecs', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'URI="audio"\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', jasmine.any(String))
.addPartialStream(ContentType.AUDIO)
.mime('audio/mp4', jasmine.any(String))
.build();
await testHlsParser(master, media, manifest);
});
it('parses multiple variants', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
'#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
'video2\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'URI="audio"\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
'URI="audio2"\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.bandwidth(200)
.addPartialStream(ContentType.VIDEO)
.size(960, 540)
.addPartialStream(ContentType.AUDIO)
.language('en')
.addPartialVariant()
.bandwidth(300)
.addPartialStream(ContentType.VIDEO)
.size(960, 540)
.addPartialStream(ContentType.AUDIO)
.language('fr')
.build();
await testHlsParser(master, media, manifest);
});
it('parses multiple streams with the same group id', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",',
'URI="audio"\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="fr",',
'URI="audio2"\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.language('en')
.addPartialStream(ContentType.VIDEO)
.addPartialStream(ContentType.AUDIO)
.language('en')
.addPartialVariant()
.language('fr')
.addPartialStream(ContentType.VIDEO)
.addPartialStream(ContentType.AUDIO)
.language('fr')
.build();
await testHlsParser(master, media, manifest);
});
it('should call filterAllPeriods for parsing', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let filterAllPeriods = jasmine.createSpy('filterAllPeriods');
playerInterface.filterAllPeriods = Util.spyFunc(filterAllPeriods);
parser.start('test:/master', playerInterface)
.then(function(manifest) {
expect(filterAllPeriods.calls.count()).toBe(1);
}).catch(fail).then(done);
});
it('gets mime type from header request', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.test',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.build();
// The extra parameters should be stripped by the parser.
fakeNetEngine.setHeaders(
'test:/main.test', {
'content-type': 'video/mp4; foo=bar',
});
await testHlsParser(master, media, manifest);
});
it('parses manifest with text streams', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'URI="audio"\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
'URI="text"\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",LANGUAGE="es",',
'URI="text2"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1",SUBTITLES="sub1"\n',
'video\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1",SUBTITLES="sub2"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const textMedia = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.vtt',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.addPartialStream(ContentType.AUDIO)
.mime('audio/mp4', 'mp4a')
.addPartialStream(ContentType.TEXT)
.language('en')
.mime('text/vtt', '')
.kind(TextStreamKind.SUBTITLE)
.addPartialStream(ContentType.TEXT)
.language('es')
.mime('text/vtt', '')
.kind(TextStreamKind.SUBTITLE)
.build();
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseText('test:/text', textMedia)
.setResponseText('test:/text2', textMedia)
.setResponseText('test:/main.vtt', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});
it('parses manifest with text streams without SUBTITLES', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'URI="audio"\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
'URI="text"\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",LANGUAGE="es",',
'URI="text2"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const textMedia = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.vtt',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.addPartialStream(ContentType.AUDIO)
.addPartialStream(ContentType.TEXT)
.mime('text/vtt', '')
.kind(TextStreamKind.SUBTITLE)
.addPartialStream(ContentType.TEXT)
.mime('text/vtt', '')
.kind(TextStreamKind.SUBTITLE)
.build();
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseText('test:/text', textMedia)
.setResponseText('test:/text2', textMedia)
.setResponseText('test:/main.vtt', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});
it('calculates duration from stream lengths', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
'URI="text"\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'CHANNELS="2",URI="audio"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1",SUBTITLES="sub1"\n',
'video\n',
].join('');
const video = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'main.mp4\n',
'#EXTINF:5,\n',
'main.mp4\n',
'#EXTINF:5,\n',
'main.mp4\n',
].join('');
const audio = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'main.mp4\n',
'#EXTINF:5,\n',
'main.mp4\n',
].join('');
const text = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.vtt',
].join('');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', audio)
.setResponseText('test:/video', video)
.setResponseText('test:/text', text)
.setResponseText('test:/main.vtt', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let actual = await parser.start('test:/master', playerInterface);
// Duration should be the minimum of the streams, but ignore the text
// stream.
let timeline = actual.presentationTimeline;
expect(timeline.getDuration()).toBe(10);
let period = actual.periods[0];
expect(period.textStreams.length).toBe(1);
expect(period.variants.length).toBe(1);
expect(period.variants[0].audio).toBeTruthy();
expect(period.variants[0].video).toBeTruthy();
});
it('parses manifest with MP4+TTML streams', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
'URI="text"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,stpp.TTML.im1t",',
'RESOLUTION=960x540,FRAME-RATE=60,SUBTITLES="sub1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.addPartialStream(ContentType.TEXT)
.language('en')
.mime('application/mp4', 'stpp.TTML.im1t')
.build();
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseText('test:/text', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});
it('detects VTT streams by codec', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
'URI="text"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,vtt",',
'RESOLUTION=960x540,FRAME-RATE=60,SUBTITLES="sub1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const textMedia = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.foo',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.addPartialStream(ContentType.TEXT)
.mime('text/vtt', 'vtt')
.build();
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseText('test:/text', textMedia)
.setResponseText('test:/main.foo', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let actual = await parser.start('test:/master', playerInterface);
expect(actual).toEqual(manifest);
});
it('allows init segments in text streams', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
'URI="text"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,wvtt",',
'RESOLUTION=960x540,FRAME-RATE=60,SUBTITLES="sub1"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(jasmine.any(Number))
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.addPartialStream(ContentType.TEXT)
.anyInitSegment()
.kind(TextStreamKind.SUBTITLE)
.build();
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseText('test:/text', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
parser.start('test:/master', playerInterface)
.then(function(actual) { expect(actual).toEqual(manifest); })
.catch(fail).then(done);
});
it('parses video described by a media tag', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.mime('video/mp4', 'avc1')
.frameRate(60)
.size(960, 540)
.addPartialStream(ContentType.AUDIO)
.build();
await testHlsParser(master, media, manifest);
});
it('constructs relative URIs', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio/audio.m3u8\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video/video.m3u8"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'segment.mp4',
].join('');
fakeNetEngine
.setResponseText('test:/host/master.m3u8', master)
.setResponseText('test:/host/audio/audio.m3u8', media)
.setResponseText('test:/host/video/video.m3u8', media)
.setResponseValue('test:/host/audio/init.mp4', initSegmentData)
.setResponseValue('test:/host/audio/segment.mp4', segmentData)
.setResponseValue('test:/host/video/init.mp4', initSegmentData)
.setResponseValue('test:/host/video/segment.mp4', segmentData);
parser.start('test:/host/master.m3u8', playerInterface)
.then(function(actual) {
let video = actual.periods[0].variants[0].video;
let audio = actual.periods[0].variants[0].audio;
let videoPosition = video.findSegmentPosition(0);
let audioPosition = audio.findSegmentPosition(0);
goog.asserts.assert(videoPosition != null,
'Cannot find first video segment');
goog.asserts.assert(audioPosition != null,
'Cannot find first audio segment');
let videoReference = video.getSegmentReference(videoPosition);
let audioReference = audio.getSegmentReference(audioPosition);
expect(videoReference).not.toBe(null);
expect(audioReference).not.toBe(null);
if (videoReference) {
expect(videoReference.getUris()[0])
.toEqual('test:/host/video/segment.mp4');
}
if (audioReference) {
expect(audioReference.getUris()[0])
.toEqual('test:/host/audio/segment.mp4');
}
}).catch(fail).then(done);
});
it('allows streams with no init segment', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:6\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'selfInit.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addPartialStream(ContentType.VIDEO)
.nullInitSegment()
.mime('video/mp4', 'avc1')
.addPartialStream(ContentType.AUDIO)
.nullInitSegment()
.mime('audio/mp4', 'mp4a')
.build();
await testHlsParser(master, media, manifest);
});
it('constructs DrmInfo for Widevine', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
].join('');
const initDataBase64 =
'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE=';
const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:6\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,',
'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",',
'URI="data:text/plain;base64,',
initDataBase64, '",\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
let manifest = new shaka.test.ManifestGenerator()
.anyTimeline()
.addPeriod(0)
.addPartialVariant()
.addDrmInfo('com.widevine.alpha')
.addCencInitData(initDataBase64)
.addPartialStream(ContentType.VIDEO)
.encrypted(true)
.build();
await testHlsParser(master, media, manifest);
});
describe('Errors out', function() {
const Code = shaka.util.Error.Code;
/**
* @param {string} master
* @param {string} media
* @param {!shaka.util.Error} error
* @param {function()} done
*/
function verifyError(master, media, error, done) {
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', media)
.setResponseText('test:/video', media)
.setResponseValue('test:/main.exe', segmentData)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
parser.start('test:/master', playerInterface)
.then(fail)
.catch(function(e) {
shaka.test.Util.expectToEqualError(e, error);
})
.then(done);
}
it('if multiple init sections were provided', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
Code.HLS_MULTIPLE_MEDIA_INIT_SECTIONS_FOUND);
verifyError(master, media, error, done);
});
it('if unable to guess mime type', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.exe',
].join('');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
Code.HLS_COULD_NOT_GUESS_MIME_TYPE, 'exe');
verifyError(master, media, error, done);
});
it('if unable to guess codecs', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="aaa,bbb",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",',
'URI="video"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
Code.HLS_COULD_NOT_GUESS_CODECS,
['aaa', 'bbb']);
verifyError(master, media, error, done);
});
describe('if required attributes are missing', function() {
/**
* @param {string} master
* @param {string} media
* @param {string} attributeName
* @param {function()} done
*/
function verifyMissingAttribute(master, media, attributeName, done) {
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
Code.HLS_REQUIRED_ATTRIBUTE_MISSING,
attributeName);
verifyError(master, media, error, done);
}
it('bandwidth', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.exe',
].join('');
verifyMissingAttribute(master, media, 'BANDWIDTH', done);
});
it('uri', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:CODECS="avc1,mp4a",BANDWIDTH=200,',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.exe',
].join('');
verifyMissingAttribute(master, media, 'URI', done);
});
});
describe('if required tags are missing', function() {
/**
* @param {string} master
* @param {string} media
* @param {string} tagName
* @param {function()} done
*/
function verifyMissingTag(master, media, tagName, done) {
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
Code.HLS_REQUIRED_TAG_MISSING,
tagName);
verifyError(master, media, error, done);
}
it('EXTINF', function(done) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n',
'audio\n',
'#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video"',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
verifyMissingTag(master, media, 'EXTINF', done);
});
});
}); // Errors out
describe('getStartTime_', function() {
/** @type {number} */
let segmentDataStartTime;
/** @type {!ArrayBuffer} */
let tsSegmentData;
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
// TODO: Add separate tests to cover correct handling of BYTERANGE in
// constructing references. Here it is covered incidentally.
const expectedStartByte = 616;
const expectedEndByte = 121705;
// Nit: this value is an implementation detail of the fix for #1106
const partialEndByte = expectedStartByte + 2048 - 1;
beforeEach(function() {
// TODO: use StreamGenerator?
segmentData = new Uint8Array([
0x00, 0x00, 0x00, 0x24, // size (36)
0x6D, 0x6F, 0x6F, 0x66, // type (moof)
0x00, 0x00, 0x00, 0x1C, // traf size (28)
0x74, 0x72, 0x61, 0x66, // type (traf)
0x00, 0x00, 0x00, 0x14, // tfdt size (20)
0x74, 0x66, 0x64, 0x74, // type (tfdt)
0x01, 0x00, 0x00, 0x00, // version and flags
0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime first 4 bytes
0x00, 0x00, 0x07, 0xd0, // baseMediaDecodeTime last 4 bytes (2000)
]).buffer;
tsSegmentData = new Uint8Array([
0x47, // TS sync byte (fixed value)
0x41, 0x01, // not corrupt, payload follows, packet ID 257
0x10, // not scrambled, no adaptation field, payload only, seq #0
0x00, 0x00, 0x01, // PES start code (fixed value)
0xe0, // stream ID (video stream 0)
0x00, 0x00, // PES packet length (doesn't matter)
0x80, // marker bits (fixed value), not scrambled, not priority
0x80, // PTS only, no DTS, other flags 0 (don't matter)
0x05, // remaining PES header length == 5 (one timestamp)
0x21, 0x00, 0x0b, 0x7e, 0x41, // PTS = 180000, encoded into 5 bytes
]).buffer;
// 180000 (TS PTS) divided by fixed TS timescale (90000) = 2s.
// 2000 (MP4 PTS) divided by parsed MP4 timescale (1000) = 2s.
segmentDataStartTime = 2;
});
it('parses start time from mp4 segment', async () => {
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let ref = ManifestParser.makeReference(
'test:/main.mp4' /* uri */,
0 /* position */,
0 /* startTime */,
5 /* endTime */,
'' /* baseUri */,
expectedStartByte,
expectedEndByte);
let manifest = await parser.start('test:/master', playerInterface);
let video = manifest.periods[0].variants[0].video;
ManifestParser.verifySegmentIndex(video, [ref]);
// Make sure the segment data was fetched with the correct byte
// range.
fakeNetEngine.expectRangeRequest(
'test:/main.mp4',
expectedStartByte,
partialEndByte);
// In VOD content, we set the presentationTimeOffset to align the
// content to presentation time 0.
expect(video.presentationTimeOffset).toEqual(segmentDataStartTime);
});
it('parses start time from ts segments', async () => {
let tsMediaPlaylist = media.replace(/\.mp4/g, '.ts');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', tsMediaPlaylist)
.setResponseValue('test:/main.ts', tsSegmentData);
let ref = ManifestParser.makeReference(
'test:/main.ts' /* uri */,
0 /* position */,
0 /* startTime */,
5 /* endTime */,
'' /* baseUri */,
expectedStartByte,
expectedEndByte);
let manifest = await parser.start('test:/master', playerInterface);
let video = manifest.periods[0].variants[0].video;
ManifestParser.verifySegmentIndex(video, [ref]);
// Make sure the segment data was fetched with the correct byte
// range.
fakeNetEngine.expectRangeRequest(
'test:/main.ts',
expectedStartByte,
partialEndByte);
// In VOD content, we set the presentationTimeOffset to align the
// content to presentation time 0.
expect(video.presentationTimeOffset).toEqual(segmentDataStartTime);
});
it('sets duration with respect to presentation offset', async () => {
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let manifest = await parser.start('test:/master', playerInterface);
let presentationTimeline = manifest.presentationTimeline;
let video = manifest.periods[0].variants[0].video;
let ref = video.getSegmentReference(0);
expect(video.getSegmentReference(1)).toBe(null); // No more references.
expect(video.presentationTimeOffset).toEqual(segmentDataStartTime);
// The duration should be set to the sum of the segment durations (5),
// even though the endTime of the segment is larger.
expect(ref.endTime - ref.startTime).toEqual(5);
expect(presentationTimeline.getDuration()).toEqual(5);
});
});
it('correctly detects VOD streams as non-live', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'main.mp4',
].join('');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let manifest = await parser.start('test:/master', playerInterface);
expect(manifest.presentationTimeline.isLive()).toBe(false);
});
it('correctly detects streams with ENDLIST as non-live', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'main.mp4\n',
'#EXT-X-ENDLIST',
].join('');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let manifest = await parser.start('test:/master', playerInterface);
expect(manifest.presentationTimeline.isLive()).toBe(false);
});
it('guesses MIME types for known extensions', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'main.mp4\n',
'#EXT-X-ENDLIST',
].join('');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let manifest = await parser.start('test:/master', playerInterface);
let video = manifest.periods[0].variants[0].video;
expect(video.mimeType).toBe('video/mp4');
});
it('guesses MIME types for known extensions with parameters', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MAP:URI="init.mp4"\n',
'#EXTINF:5,\n',
'main.mp4?foo=bar\n',
'#EXT-X-ENDLIST',
].join('');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4?foo=bar', segmentData);
let manifest = await parser.start('test:/master', playerInterface);
let video = manifest.periods[0].variants[0].video;
expect(video.mimeType).toBe('video/mp4');
});
it('does not produce multiple Streams for one playlist', async () => {
// Regression test for a bug in our initial HLS live implementation
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",URI="audio"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=400,CODECS="avc1,mp4a",',
'RESOLUTION=1280x720,AUDIO="audio"\n',
'video0\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=768x432,AUDIO="audio"\n',
'video1\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video0', media)
.setResponseText('test:/video1', media)
.setResponseText('test:/audio', media)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
let manifest = await parser.start('test:/master', playerInterface);
expect(manifest.periods[0].variants.length).toBe(2);
let audio0 = manifest.periods[0].variants[0].audio;
let audio1 = manifest.periods[0].variants[1].audio;
// These should be the exact same memory address, not merely equal.
// Otherwise, the parser will only be replacing one of the SegmentIndexes
// on update, which will lead to live streaming issues.
expect(audio0).toBe(audio1);
});
// https://github.com/google/shaka-player/issues/1664
it('correctly resolves relative playlist URIs', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",URI="audio"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=400,CODECS="avc1,mp4a",',
'RESOLUTION=1280x720,AUDIO="audio"\n',
'video\n',
].join('');
const media = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'#EXT-X-BYTERANGE:121090@616\n',
'main.mp4',
].join('');
fakeNetEngine
.setResponseText('media/master', master) // Relative master URI
.setResponseText('http://foo/media/audio', media)
.setResponseText('http://foo/media/video', media)
.setResponseValue('http://foo/media/init.mp4', initSegmentData)
.setResponseValue('http://foo/media/main.mp4', segmentData);
fakeNetEngine.setResponseFilter(function(type, response) {
// Simulate support for relative URIs in the browser by setting the
// absolute URI in response.uri.
if (response.uri == 'media/master') {
response.uri = 'http://foo/media/master';
}
});
// When this test fails, parser.start() fails. The relative playlist URI was
// being resolved to a bogus location ('media/media/audio'), which resulted
// in a failed request. Even if that bogus location were made absolute, it
// would still be wrong.
const manifest =
await parser.start('media/master', playerInterface);
expect(manifest.periods[0].variants.length).toBe(1);
});
});