mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-26 17:46:26 +03:00
f32767578a
Normally, we expect that only media playlists contain segment tags (EXT-X-KEY, EXTINF, etc). We determine if a playlist is a media playlist by the presence of an appropriate playlist tag (EXT-X-MAP, EXT-X-PLAYLIST-TYPE, etc). However, we were checking tags one-by-one, on a single while loop. This meant that we would reject manifests where a segment tag appears before a playlist tag, as we did not yet know that the playlist was a media playlist. This changes the manifest text parser so that it checks for playlist type first. Issue #1382 Change-Id: I1b9f674a58c60ab291d6d004462b0c0cae49631d
269 lines
7.7 KiB
JavaScript
269 lines
7.7 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} uri
|
|
* @return {!shaka.hls.Playlist}
|
|
* @throws {shaka.util.Error}
|
|
*/
|
|
shaka.hls.ManifestTextParser.prototype.parsePlaylist = function(data, uri) {
|
|
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();
|
|
|
|
let 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])) {
|
|
let 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.indexOf(tag.name) >= 0) {
|
|
playlistType = shaka.hls.PlaylistType.MEDIA;
|
|
break;
|
|
} else if (tag.name == 'EXT-X-STREAM-INF') {
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** {Array.<shaka.hls.Tag>} */
|
|
let tags = [];
|
|
for (let i = 1; i < lines.length;) {
|
|
// Skip comments
|
|
if (shaka.hls.Utils.isComment(lines[i])) {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
let tag = this.parseTag_(lines[i]);
|
|
if (SEGMENT_TAGS.indexOf(tag.name) >= 0) {
|
|
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);
|
|
}
|
|
|
|
let segmentsData = lines.splice(i, lines.length - i);
|
|
let segments = this.parseSegments_(segmentsData, tags);
|
|
return new shaka.hls.Playlist(uri, 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') {
|
|
let tagUri = new shaka.hls.Attribute('URI', lines[i]);
|
|
tag.addAttribute(tagUri);
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
return new shaka.hls.Playlist(uri, playlistType, tags);
|
|
};
|
|
|
|
|
|
/**
|
|
* Parses an array of strings into an array of HLS Segment objects.
|
|
*
|
|
* @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(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
|
|
.indexOf(tag.name) >= 0) {
|
|
playlistTags.push(tag);
|
|
} else {
|
|
segmentTags.push(tag);
|
|
}
|
|
} else if (shaka.hls.Utils.isComment(line)) {
|
|
// Skip comments.
|
|
return [];
|
|
} else {
|
|
let uri = line.trim();
|
|
// The uri appears after all of the tags describing the segment.
|
|
let segment = new shaka.hls.Segment(uri, 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.indexOf('=') >= 0) {
|
|
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'
|
|
];
|