UNPKG

shaka-player

Version:
613 lines (533 loc) 19.3 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('ManifestTextParser', () => { /** @type {!shaka.hls.ManifestTextParser} */ let parser; beforeEach(() => { parser = new shaka.hls.ManifestTextParser(); }); describe('parsePlaylist', () => { it('rejects invalid playlists', () => { verifyError('invalid playlist', shaka.util.Error.Code.HLS_PLAYLIST_HEADER_MISSING); // This Master playlist is invalid cause it contains a segment tag. // All segment information should be in a Media playlist. verifyError('#EXTM3U\n' + '#EXT-X-MEDIA:TYPE=AUDIO\n' + '#EXTINF:6.00600', shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY); }); it('parses a Media Playlist', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-TARGETDURATION:6\n', // manifest URI: 'https://test/manifest.m3u8'); }); it('parses a Master Playlist', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), new shaka.hls.Tag(/* id= */ 1, 'EXT-X-STREAM-INF', [ new shaka.hls.Attribute('BANDWIDTH', '2165224'), new shaka.hls.Attribute('URI', 'prog_index.m3u8'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-TARGETDURATION:6\n' + '#EXT-X-STREAM-INF:BANDWIDTH=2165224\n' + 'prog_index.m3u8', // manifest URI: 'https://test/manifest.m3u8'); }); it('ignores comments', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), ], }, // playlist text: '#EXTM3U\n' + '#Comment\n' + '#EXT-X-TARGETDURATION:6', // manifest URI: 'https://test/manifest.m3u8'); }); /** * @param {string} string * @param {shaka.util.Error.Code} code */ function verifyError(string, code) { const data = shaka.util.StringUtils.toUTF8(string); const error = shaka.test.Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, code)); expect(() => parser.parsePlaylist(data, /* uri= */ '')).toThrow(error); } }); describe('parseTag', () => { it('parses tags with no attributes', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MASTER, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-INDEPENDENT-SEGMENTS', []), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-INDEPENDENT-SEGMENTS', // manifest URI: 'https://test/manifest.m3u8'); verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 1, 'EXT-X-PLAYLIST-TYPE', [], 'VOD'), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-PLAYLIST-TYPE:VOD', // manifest URI: 'https://test/manifest.m3u8'); verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 2, 'EXT-X-MEDIA-SEQUENCE', [], '1'), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:1', // manifest URI: 'https://test/manifest.m3u8'); }); it('parses tags with attributes', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MASTER, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA', [new shaka.hls.Attribute('TYPE', 'CLOSED-CAPTIONS')]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS', // manifest URI: 'https://test/manifest.m3u8'); verifyPlaylist( { type: shaka.hls.PlaylistType.MASTER, tags: [ new shaka.hls.Tag(/* id= */ 1, 'EXT-X-MEDIA', [ new shaka.hls.Attribute('URI', 'main.mp4'), new shaka.hls.Attribute('BYTERANGE', '720@0'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA:URI="main.mp4",BYTERANGE="720@0"', // manifest URI: 'https://test/manifest.m3u8'); }); it('parses tags with commas in attribute values', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MASTER, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA', [ new shaka.hls.Attribute('CODECS', 'avc1.64002a,mp4a.40.2'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA:CODECS="avc1.64002a,mp4a.40.2"', // manifest URI: 'https://test/manifest.m3u8'); verifyPlaylist( { type: shaka.hls.PlaylistType.MASTER, tags: [ new shaka.hls.Tag(/* id= */ 1, 'EXT-X-MEDIA', [ new shaka.hls.Attribute('CODECS', 'avc1.64002a,mp4a.40.2,avc2.64000'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA:CODECS="avc1.64002a,mp4a.40.2,avc2.64000"', // manifest URI: 'https://test/manifest.m3u8'); verifyPlaylist( { type: shaka.hls.PlaylistType.MASTER, tags: [ new shaka.hls.Tag(/* id= */ 2, 'EXT-X-MEDIA', [ new shaka.hls.Attribute('CODECS', 'avc1.64002a,mp4a.40.2'), new shaka.hls.Attribute('AUDIO', 'a1,a2'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA:CODECS="avc1.64002a,mp4a.40.2",AUDIO="a1,a2"', // manifest URI: 'https://test/manifest.m3u8'); verifyPlaylist( { type: shaka.hls.PlaylistType.MASTER, tags: [ new shaka.hls.Tag(/* id= */ 3, 'EXT-X-MEDIA', [ new shaka.hls.Attribute('CODECS', 'av01.0.08M.08,mp4a.40.2'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA:CODECS="av01.0.08M.08,mp4a.40.2"', // manifest URI: 'https://test/manifest.m3u8'); }); it('rejects invalid tags', () => { const error = shaka.test.Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.INVALID_HLS_TAG, 'invalid tag')); const text = shaka.util.StringUtils.toUTF8('#EXTM3U\ninvalid tag'); expect(() => parser.parsePlaylist(text, /* uri= */ '')).toThrow(error); }); }); describe('tag.toString', () => { it('recreates valid tag with attributes', () => { const text = '#EXT-X-MEDIA:CODECS="avc1.64002a,mp4a.40.2",AUDIO="a1,a2"'; const tag = shaka.hls.ManifestTextParser.parseTag(0, text); expect(text).toBe(tag.toString()); }); it('recreates valid tag with value', () => { const text = '#EXT-X-PLAYLIST-TYPE:VOD'; const tag = shaka.hls.ManifestTextParser.parseTag(0, text); expect(text).toBe(tag.toString()); }); it('recreates valid tag with no value', () => { const text = '#EXTM3U'; const tag = shaka.hls.ManifestTextParser.parseTag(0, text); expect(text).toBe(tag.toString()); }); it('recreates valid tag with both value and attributes', () => { const text = '#EXTINF:5.99467,pid=180'; const tag = shaka.hls.ManifestTextParser.parseTag(0, text); expect(text).toBe(tag.toString()); }); }); describe('parseSegments', () => { it('parses segments', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA-SEQUENCE', [], '1'), ], segments: [ new shaka.hls.Segment('https://test/manifest.m3u8', 'https://test/test.mp4', [ new shaka.hls.Tag(/* id= */ 2, 'EXTINF', [], '5.99467'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:1\n' + '#EXTINF:5.99467\n' + 'https://test/test.mp4\n', // manifest URI: 'https://test/manifest.m3u8'); }); it('handles tags with both value and attributes', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA-SEQUENCE', [], '1'), ], segments: [ new shaka.hls.Segment('https://test/manifest.m3u8', 'https://test/test.mp4', [ new shaka.hls.Tag( /* id= */ 2, 'EXTINF', [new shaka.hls.Attribute('pid', '180')], '5.99467'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:1\n' + '#EXTINF:5.99467,pid=180\n' + 'https://test/test.mp4\n', // manifest URI: 'https://test/manifest.m3u8'); }); it('handles manifests with a segment tag before a playlist tag', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 2, 'EXT-X-TARGETDURATION', [], '6'), ], segments: [ new shaka.hls.Segment('https://test/manifest.m3u8', 'https://test/test.mp4', [ new shaka.hls.Tag(/* id= */ 1, 'EXT-X-KEY', [ new shaka.hls.Attribute('METHOD', 'AES-128'), new shaka.hls.Attribute('URI', 'http://key.com'), new shaka.hls.Attribute('IV', '123'), ]), new shaka.hls.Tag(/* id= */ 3, 'EXTINF', [], '5.99467'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-KEY:METHOD="AES-128",URI="http://key.com",IV="123"\n' + '#EXT-X-TARGETDURATION:6\n' + '#EXTINF:5.99467\n' + 'https://test/test.mp4\n', // manifest URI: 'https://test/manifest.m3u8'); }); it('tracks playlist URI', () => { verifyPlaylist( { absoluteUri: 'https://test/manifest.m3u8', type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA-SEQUENCE', [], '1'), ], segments: [ new shaka.hls.Segment('https://test/manifest.m3u8', 'test.mp4', [ new shaka.hls.Tag(/* id= */ 2, 'EXTINF', [], '5.99467'), ]), ], }, // playlist text: '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:1\n' + '#EXTINF:5.99467\n' + 'test.mp4\n', // manifest URI: 'https://test/manifest.m3u8'); }); }); describe('parseSegments', () => { const manifestText = '#EXTM3U\n' + '#EXT-X-TARGETDURATION:6\n' + '#EXTINF:5\n' + 'uri\n' + '#EXTINF:4\n' + 'uri2\n'; it('parses segments', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), ], segments: [ new shaka.hls.Segment('https://test/manifest.m3u8', 'uri', [new shaka.hls.Tag(2, 'EXTINF', [], '5')]), new shaka.hls.Segment('https://test/manifest.m3u8', 'uri2', [new shaka.hls.Tag(3, 'EXTINF', [], '4')]), ], }, manifestText, // manifest URI: 'https://test/manifest.m3u8'); }); it('identifies playlist tags', () => { verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), new shaka.hls.Tag(/* id= */ 4, 'EXT-X-ENDLIST', []), ], segments: [ new shaka.hls.Segment('https://test/manifest.m3u8', 'uri', [new shaka.hls.Tag(2, 'EXTINF', [], '5')]), new shaka.hls.Segment('https://test/manifest.m3u8', 'uri2', [new shaka.hls.Tag(3, 'EXTINF', [], '4')]), ], }, // Append a playlist tag to the manifest text so it appears after // segment-related tags. manifestText + '#EXT-X-ENDLIST', // manifest URI: 'https://test/manifest.m3u8'); }); it('parses segments with partial segments', () => { const manifestTextWithPartialSegments = '#EXTM3U\n' + '#EXT-X-TARGETDURATION:6\n' + '#EXT-X-MAP:URI="init.mp4"\n' + '#EXTINF:5\n' + 'uri\n' + '#EXT-X-PART:DURATION=1,URI="uri2.1"\n' + '#EXT-X-PART:DURATION=1,URI="uri2.2"\n' + // partialSegments1 '#EXTINF:2\n' + 'uri2\n' + '#EXT-X-PART:DURATION=1,URI="uri3.1"\n'; // partialSegments2 const mapTag = new shaka.hls.Tag(/* id= */ 2, 'EXT-X-MAP', /* attributes= */ [new shaka.hls.Attribute('URI', 'init.mp4')]); const partialSegments1 = [ new shaka.hls.Tag(/* id= */ 4, 'EXT-X-PART', [ new shaka.hls.Attribute('DURATION', '1'), new shaka.hls.Attribute('URI', 'uri2.1'), ]), new shaka.hls.Tag(/* id= */ 5, 'EXT-X-PART', [ new shaka.hls.Attribute('DURATION', '1'), new shaka.hls.Attribute('URI', 'uri2.2'), ]), ]; const partialSegments2 = [ new shaka.hls.Tag(/* id= */ 7, 'EXT-X-PART', [ new shaka.hls.Attribute('DURATION', '1'), new shaka.hls.Attribute('URI', 'uri3.1'), ]), ]; verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), ], segments: [ new shaka.hls.Segment( /* absoluteMediaPlaylistUri= */ 'https://test/manifest.m3u8', /* verbatimSegmentUri= */ 'uri', /* tags= */ [ new shaka.hls.Tag(3, 'EXTINF', [], '5'), mapTag, ]), new shaka.hls.Segment( /* absoluteMediaPlaylistUri= */ 'https://test/manifest.m3u8', /* verbatimSegmentUri= */ 'uri2', /* tags= */ [ new shaka.hls.Tag(6, 'EXTINF', [], '2'), mapTag, ], /* partialSegments= */ partialSegments1), new shaka.hls.Segment( /* absoluteMediaPlaylistUri= */ '', /* verbatimSegmentUri= */ '', /* tags= */ [mapTag], /* partialSegments= */ partialSegments2), ], }, manifestTextWithPartialSegments, // manifest URI: 'https://test/manifest.m3u8'); }); it('parses segments with preload hint segments', () => { // const manifestTextWithPreloadSegments = '#EXTM3U\n' + // '#EXT-X-TARGETDURATION:6\n' + // '#EXT-X-MAP:URI="init.mp4"\n' + // '#EXTINF:5\n' + // 'uri\n' + // '#EXT-X-PRELOAD-HINT:TYPE=MAP,URI="init.mp4\n' + // '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="uri2.1\n'; const manifestTextWithPreloadSegments = '#EXTM3U\n' + '#EXT-X-TARGETDURATION:6\n' + '#EXT-X-MAP:URI="init.mp4"\n' + // mapTag '#EXTINF:5\n' + 'uri\n' + '#EXT-X-PRELOAD-HINT:TYPE=MAP,URI="init.mp4"\n' + // preloadMapTag '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="uri2.1"\n'; // 1st preloadSegment const mapTag = new shaka.hls.Tag(/* id= */ 2, 'EXT-X-MAP', /* attributes= */ [new shaka.hls.Attribute('URI', 'init.mp4')]); const preloadMapTag = new shaka.hls.Tag(/* id= */ 4, 'EXT-X-MAP', /* attributes= */ [ new shaka.hls.Attribute('TYPE', 'MAP'), new shaka.hls.Attribute('URI', 'init.mp4'), ]); const preloadSegment = [ new shaka.hls.Tag(/* id= */ 5, 'EXT-X-PRELOAD-HINT', [ new shaka.hls.Attribute('TYPE', 'PART'), new shaka.hls.Attribute('URI', 'uri2.1'), ]), ]; verifyPlaylist( { type: shaka.hls.PlaylistType.MEDIA, tags: [ new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), ], segments: [ new shaka.hls.Segment( /* absoluteMediaPlaylistUri= */ 'https://test/manifest.m3u8', /* verbatimSegmentUri= */ 'uri', /* tags= */ [ new shaka.hls.Tag(3, 'EXTINF', [], '5'), mapTag, ]), new shaka.hls.Segment( /* absoluteMediaPlaylistUri= */ '', /* verbatimSegmentUri= */ '', /* tags= */ [preloadMapTag], /* partialSegments= */ preloadSegment), ], }, manifestTextWithPreloadSegments, // manifest URI: 'https://test/manifest.m3u8'); }); }); // TODO(#1672): Get a better type than "Object" here. /** * @param {!Object} expectedPlaylist * @param {string} playlistText * @param {string} absoluteManifestUri */ function verifyPlaylist(expectedPlaylist, playlistText, absoluteManifestUri) { const playlistBuffer = shaka.util.StringUtils.toUTF8(playlistText); const actualPlaylist = parser.parsePlaylist(playlistBuffer, absoluteManifestUri); expect(actualPlaylist).toEqual(jasmine.objectContaining(expectedPlaylist)); } });