Files
shaka-player/lib/hls/manifest_text_parser.js
T
Joey Parrish 69d3193675 Clarify and clean up usage of URIs in HLS
This fixes issues with the interpretation of URIs in HLS and makes
their usage and meaning consistent and clear.

 - Name URI variables as either absolute, final (post-redirect), or
   verbatim (exactly as they appear in the playlist)
 - Identify media playlists by their verbatim URI when testing for
   equality or duplication
 - When a master playlist is redirected, interpret media playlists as
   relative to the redirected location
 - When a media playlist is redirected, request updates from the
   redirected location
 - When updating a media playlist, resolve media segment URIs as
   relative to the latest redirected media playlist URI
 - Resolve absolute segment URIs when parsing the playlist text,
   rather than waiting until we intepret and build the manifest
 - Remove some incidental bind() calls, which exposed compiler errors
 - Avoid refactoring long parameter lists
 - Avoid refactoring for async/await
 - Clean up redirection tests, which were brittle and did not verify
   what they seemed to
 - Use relative segment URIs for all segments in tests
 - Use media playlist URIs within master playlists in tests
 - Add a regression test specifically for #1664

Closes #1664

Change-Id: I45f946790c7d669637c231ae93920a09c18c4222
2018-11-19 12:37:50 -08:00

274 lines
8.0 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.
*/
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>) 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 should be parsed as attributes if it contains '='.
3. Otherwise, this is a value.
4. If there is no ":", it's a simple tag with no attributes and no value */
let 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);
}
let name = blocks[1];
let data = blocks[2];
let attributes = [];
if (data && data.includes('=')) {
let parser = new shaka.util.TextParser(data);
let blockAttrs;
// 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 regex = /([^=]+)=(?:"([^"]*)"|([^",]*))(?:,|$)/g;
while ((blockAttrs = parser.readRegex(regex))) {
let attrName = blockAttrs[1];
let attrValue = blockAttrs[2] || blockAttrs[3];
let attribute = new shaka.hls.Attribute(attrName, attrValue);
attributes.push(attribute);
}
} else if (data) {
return new shaka.hls.Tag(id, name, attributes, data);
}
return new shaka.hls.Tag(id, name, attributes);
};
/**
* 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',
];