Skip to content

Commit

Permalink
Add HLS objects and basic routines to parse manifests into them.
Browse files Browse the repository at this point in the history
Issue shaka-project#279.

Change-Id: I5b6f90b682d77849ce075a2a76a4202c56e5d882
  • Loading branch information
ismena committed Jan 9, 2017
1 parent 74d205c commit 8afadb4
Show file tree
Hide file tree
Showing 7 changed files with 560 additions and 0 deletions.
4 changes: 4 additions & 0 deletions build/types/hls
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The HLS manifest parser plugin.

+../../lib/hls/hls_classes.js
+../../lib/hls/hls_parser.js
1 change: 1 addition & 0 deletions build/types/manifests
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# All standard manifest parser plugins.

+@dash
+@hls
+@offline
105 changes: 105 additions & 0 deletions lib/hls/hls_classes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @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.Attribute');
goog.provide('shaka.hls.Playlist');
goog.provide('shaka.hls.PlaylistType');
goog.provide('shaka.hls.Tag');



/**
* Creates an HLS playlist object.
*
* @param {!shaka.hls.PlaylistType} type
* @param {Array.<shaka.hls.Tag>} tags
* @param {Array.<string>=} opt_segmentsData
*
* @constructor
* @struct
*/
shaka.hls.Playlist = function(type, tags, opt_segmentsData) {
/** @const {shaka.hls.PlaylistType} */
this.type = type;

/** @const {Array.<shaka.hls.Tag>} */
this.tags = tags;

/** @const {Array.<string>} */
this.segmentsData = opt_segmentsData || null;
};


/**
* @enum {number}
*/
shaka.hls.PlaylistType = {
MASTER: 0,
MEDIA: 1
};



/**
* Creates an HLS tag object.
*
* @param {!string} name
* @param {!Array.<shaka.hls.Attribute>} attributes
* @param {?string=} opt_value
*
* @constructor
* @struct
*/
shaka.hls.Tag = function(name, attributes, opt_value) {
/** @const {!string} */
this.name = name;

/** @const {!Array.<shaka.hls.Attribute>} */
this.attributes = attributes;

/** @const {?string} */
this.value = opt_value || null;
};



/**
* Creates an HLS attribute object.
*
* @param {!string} name
* @param {!string} value
*
* @constructor
* @struct
*/
shaka.hls.Attribute = function(name, value) {
/** @const {!string} */
this.name = name;

/** @const {!string} */
this.value = value;
};


/**
* Adds an attribute to an HLS Tag.
*
* @param {!shaka.hls.Attribute} attribute
*/
shaka.hls.Tag.prototype.addAttribute = function(attribute) {
this.attributes.push(attribute);
};
194 changes: 194 additions & 0 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* @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.HlsParser');

goog.require('shaka.hls.Attribute');
goog.require('shaka.hls.Playlist');
goog.require('shaka.hls.PlaylistType');
goog.require('shaka.hls.Tag');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TextParser');



/**
* Creates a new HLS parser.
*
* @struct
* @constructor
*/
// TODO: make it implement {shakaExtern.ManifestParser}
shaka.hls.HlsParser = function() {};


/**
* @param {ArrayBuffer} data
* @return {!shaka.hls.Playlist}
* @throws {shaka.util.Error}
*/
// TODO: make private
shaka.hls.HlsParser.prototype.parsePlaylist = function(data) {
// Get the input as a string. Normalize newlines to \n.
var str = shaka.util.StringUtils.fromUTF8(data);
str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n');

var lines = str.split(/\n+/m);

if (!/^#EXTM3U($|[ \t\n])/m.test(lines[0])) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_PLAYLIST_HEADER_MISSING);
}

var HlsParser = shaka.hls.HlsParser;

/** shaka.hls.PlaylistType */
var playlistType = shaka.hls.PlaylistType.MASTER;

/** {Array.<shaka.hls.Tag>} */
var tags = [];

var i = 1;
while (i < lines.length) {
// Skip comments
if (/^#(?!EXT)/m.test(lines[i])) {
i += 1;
continue;
}

var tag = this.parseTag_(lines[i]);


if (HlsParser.MEDIA_PLAYLIST_TAGS.indexOf(tag.name) >= 0) {
playlistType = shaka.hls.PlaylistType.MEDIA;
} else if (HlsParser.SEGMENT_TAGS.indexOf(tag.name) >= 0) {
if (playlistType != shaka.hls.PlaylistType.MEDIA) {
// Only media playlist should contain segment tags
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
}

var segmentsData = lines.splice(i, lines.length - i);
return new shaka.hls.Playlist(playlistType, tags, segmentsData);
}

tags.push(tag);
i += 1;

// EXT-X-STREAM-INF tag is followed by a uri of a media playlist.
// Add uri to the tag object.
if (tag.name == 'EXT-X-STREAM-INF') {
var uri = new shaka.hls.Attribute('URI', lines[i]);
tag.addAttribute(uri);
i += 1;
}
}

return new shaka.hls.Playlist(playlistType, tags);
};


/**
* Parses a string into an HLS Tag object.
*
* @param {!string} word
* @return {!shaka.hls.Tag}
* @throws {shaka.util.Error}
* @private
*/
shaka.hls.HlsParser.prototype.parseTag_ = function(word) {
/* HLS tags start with '#EXT'. A tag can have a set of attributes
(#EXT-<tagname>:<attribute list>) or a value (#EXT-<tagname>:<value>).
Attributes' 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 */

var blocks = word.match(/^#(EXT[^:]*)(?::(.*))?$/);
if (!blocks) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.INVALID_HLS_TAG);
}
var name = blocks[1];
var data = blocks[2];
var attributes = [];

if (data && data.indexOf('=') >= 0) {
var parser = new shaka.util.TextParser(data);
var 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
var regex = /([^=]+)=(?:"([^"]*)"|([^",]*))(?:,|$)/g;
while (blockAttrs = parser.readRegex(regex)) {
var attrName = blockAttrs[1];
var attrValue = blockAttrs[2] || blockAttrs[3];
var attribute = new shaka.hls.Attribute(attrName, attrValue);
attributes.push(attribute);
}
} else if (data) {
return new shaka.hls.Tag(name, attributes, data);
}

return new shaka.hls.Tag(name, attributes);
};


/**
* HLS tags that only appear on Media Playlists.
* Used to determine a playlist type.
*
* @const {!Array<!string>}
*/
shaka.hls.HlsParser.MEDIA_PLAYLIST_TAGS = [
'EXT-X-TARGETDURATION',
'EXT-X-MEDIA-SEQUENCE',
'EXT-X-DISCONTINUITY-SEQUENCE',
'EXT-X-PLAYLIST-TYPE',
'EXT-X-I-FRAMES-ONLY'
];


/**
* 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.HlsParser.SEGMENT_TAGS = [
'EXTINF',
'EXT-X-BYTERANGE',
'EXT-X-DISCONTINUITY',
'EXT-X-KEY',
'EXT-X-MAP',
'EXT-X-PROGRAM-DATE-TIME',
'EXT-X-DATERANGE'
];
14 changes: 14 additions & 0 deletions lib/util/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,20 @@ shaka.util.Error.Code = {
*/
'NO_PERIODS': 4014,

/**
* HLS playlist doesn't start with a mandory #EXTM3U tag.
*/
'HLS_PLAYLIST_HEADER_MISSING': 4015,

/**
* HLS tag has an invalid name that doesn't start with '#EXT'
*/
'INVALID_HLS_TAG': 4016,

/**
* HLS playlist has both Master and Media/Segment tags.
*/
'HLS_INVALID_PLAYLIST_HIERARCHY': 4017,

// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
// RETIRED: 'INVALID_SEGMENT_INDEX': 5001,
Expand Down
1 change: 1 addition & 0 deletions shaka-player.uncompiled.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ goog.require('shaka.abr.SimpleAbrManager');
goog.require('shaka.cast.CastProxy');
goog.require('shaka.cast.CastReceiver');
goog.require('shaka.dash.DashParser');
goog.require('shaka.hls.HlsParser');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.ManifestParser');
Expand Down
Loading

0 comments on commit 8afadb4

Please sign in to comment.