UNPKG

shaka-player

Version:
285 lines (248 loc) 8.48 kB
/** * @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. */ goog.provide('shaka.hls.ManifestTextParser'); goog.require('shaka.hls.Attribute'); goog.require('shaka.hls.Playlist'); goog.require('shaka.hls.PlaylistType'); goog.require('shaka.hls.Segment'); goog.require('shaka.hls.Tag'); goog.require('shaka.hls.Utils'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.TextParser'); /** * Creates a new ManifestTextParser. * * @constructor * @struct */ shaka.hls.ManifestTextParser = function() { /** @private {number} */ this.globalId_ = 0; }; /** * @param {ArrayBuffer} data * @param {string} absolutePlaylistUri An absolute, final URI after redirects. * @return {!shaka.hls.Playlist} * @throws {shaka.util.Error} */ shaka.hls.ManifestTextParser.prototype.parsePlaylist = function(data, absolutePlaylistUri) { const MEDIA_PLAYLIST_TAGS = shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS; const SEGMENT_TAGS = shaka.hls.ManifestTextParser.SEGMENT_TAGS; // Get the input as a string. Normalize newlines to \n. let str = shaka.util.StringUtils.fromUTF8(data); str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n').trim(); const lines = str.split(/\n+/m); if (!/^#EXTM3U($|[ \t\n])/m.test(lines[0])) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.HLS_PLAYLIST_HEADER_MISSING); } /** shaka.hls.PlaylistType */ let playlistType = shaka.hls.PlaylistType.MASTER; // First, look for media playlist tags, so that we know what the playlist // type really is before we start parsing. for (let i = 1; i < lines.length; i++) { // Ignore comments. if (!shaka.hls.Utils.isComment(lines[i])) { const tag = this.parseTag_(lines[i]); // These tags won't actually be used, so don't increment the global id. this.globalId_ -= 1; if (MEDIA_PLAYLIST_TAGS.includes(tag.name)) { playlistType = shaka.hls.PlaylistType.MEDIA; break; } else if (tag.name == 'EXT-X-STREAM-INF') { i += 1; } } } /** {Array.<shaka.hls.Tag>} */ const tags = []; for (let i = 1; i < lines.length;) { // Skip comments if (shaka.hls.Utils.isComment(lines[i])) { i += 1; continue; } const tag = this.parseTag_(lines[i]); if (SEGMENT_TAGS.includes(tag.name)) { if (playlistType != shaka.hls.PlaylistType.MEDIA) { // Only media playlists should contain segment tags throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY); } const segmentsData = lines.splice(i, lines.length - i); const segments = this.parseSegments_( absolutePlaylistUri, segmentsData, tags); return new shaka.hls.Playlist( absolutePlaylistUri, playlistType, tags, segments); } tags.push(tag); i += 1; // An EXT-X-STREAM-INF tag is followed by a URI of a media playlist. // Add the URI to the tag object. if (tag.name == 'EXT-X-STREAM-INF') { const tagUri = new shaka.hls.Attribute('URI', lines[i]); tag.addAttribute(tagUri); i += 1; } } return new shaka.hls.Playlist(absolutePlaylistUri, playlistType, tags); }; /** * Parses an array of strings into an array of HLS Segment objects. * * @param {string} absoluteMediaPlaylistUri * @param {!Array.<string>} lines * @param {!Array.<!shaka.hls.Tag>} playlistTags * @return {!Array.<shaka.hls.Segment>} * @private * @throws {shaka.util.Error} */ shaka.hls.ManifestTextParser.prototype.parseSegments_ = function(absoluteMediaPlaylistUri, lines, playlistTags) { /** @type {!Array.<shaka.hls.Segment>} */ let segments = []; /** @type {!Array.<shaka.hls.Tag>} */ let segmentTags = []; lines.forEach((line) => { if (/^(#EXT)/.test(line)) { let tag = this.parseTag_(line); if (shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS.includes(tag.name)) { playlistTags.push(tag); } else { segmentTags.push(tag); } } else if (shaka.hls.Utils.isComment(line)) { // Skip comments. return []; } else { const verbatimSegmentUri = line.trim(); const absoluteSegmentUri = shaka.hls.Utils.constructAbsoluteUri( absoluteMediaPlaylistUri, verbatimSegmentUri); // The URI appears after all of the tags describing the segment. const segment = new shaka.hls.Segment(absoluteSegmentUri, segmentTags); segments.push(segment); segmentTags = []; } }); return segments; }; /** * Parses a string into an HLS Tag object while tracking what id to use next. * * @param {string} word * @return {!shaka.hls.Tag} * @throws {shaka.util.Error} * @private */ shaka.hls.ManifestTextParser.prototype.parseTag_ = function(word) { return shaka.hls.ManifestTextParser.parseTag(this.globalId_++, word); }; /** * Parses a string into an HLS Tag object. * * @param {number} id * @param {string} word * @return {!shaka.hls.Tag} * @throws {shaka.util.Error} */ shaka.hls.ManifestTextParser.parseTag = function(id, word) { /* HLS tags start with '#EXT'. A tag can have a set of attributes (#EXT-<tagname>:<attribute list>) and/or a value (#EXT-<tagname>:<value>). An attribute's format is 'AttributeName=AttributeValue'. The parsing logic goes like this: 1. Everything before ':' is a name (we ignore '#'). 2. Everything after ':' is a list of comma-seprated items, 2a. The first item might be a value, if it does not contain '='. 2b. Otherwise, items are attributes. 3. If there is no ":", it's a simple tag with no attributes and no value */ const blocks = word.match(/^#(EXT[^:]*)(?::(.*))?$/); if (!blocks) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.INVALID_HLS_TAG, word); } const name = blocks[1]; const data = blocks[2]; const attributes = []; let value; if (data) { const parser = new shaka.util.TextParser(data); let blockAttrs; // Regex: any number of non-equals-sign characters at the beginning // terminated by comma or end of line const valueRegex = /^([^,=]+)(?:,|$)/g; const blockValue = parser.readRegex(valueRegex); if (blockValue) { value = blockValue[1]; } // Regex: // 1. Key name ([1]) // 2. Equals sign // 3. Either: // a. A quoted string (everything up to the next quote, [2]) // b. An unquoted string // (everything up to the next comma or end of line, [3]) // 4. Either: // a. A comma // b. End of line const attributeRegex = /([^=]+)=(?:"([^"]*)"|([^",]*))(?:,|$)/g; while ((blockAttrs = parser.readRegex(attributeRegex))) { const attrName = blockAttrs[1]; const attrValue = blockAttrs[2] || blockAttrs[3]; const attribute = new shaka.hls.Attribute(attrName, attrValue); attributes.push(attribute); } } return new shaka.hls.Tag(id, name, attributes, value); }; /** * HLS tags that only appear on Media Playlists. * Used to determine a playlist type. * * @const {!Array.<string>} */ shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [ 'EXT-X-TARGETDURATION', 'EXT-X-MEDIA-SEQUENCE', 'EXT-X-DISCONTINUITY-SEQUENCE', 'EXT-X-PLAYLIST-TYPE', 'EXT-X-MAP', 'EXT-X-I-FRAMES-ONLY', 'EXT-X-ENDLIST', ]; /** * HLS tags that only appear on Segments in a Media Playlists. * Used to determine the start of the segments info. * * @const {!Array.<string>} */ shaka.hls.ManifestTextParser.SEGMENT_TAGS = [ 'EXTINF', 'EXT-X-BYTERANGE', 'EXT-X-DISCONTINUITY', 'EXT-X-PROGRAM-DATE-TIME', 'EXT-X-KEY', 'EXT-X-DATERANGE', ];