Source: lib/hls/manifest_text_parser.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.hls.ManifestTextParser');
  18. goog.require('shaka.hls.Attribute');
  19. goog.require('shaka.hls.Playlist');
  20. goog.require('shaka.hls.PlaylistType');
  21. goog.require('shaka.hls.Segment');
  22. goog.require('shaka.hls.Tag');
  23. goog.require('shaka.hls.Utils');
  24. goog.require('shaka.util.Error');
  25. goog.require('shaka.util.StringUtils');
  26. goog.require('shaka.util.TextParser');
  27. /**
  28. * Creates a new ManifestTextParser.
  29. *
  30. * @constructor
  31. * @struct
  32. */
  33. shaka.hls.ManifestTextParser = function() {
  34. /** @private {number} */
  35. this.globalId_ = 0;
  36. };
  37. /**
  38. * @param {!ArrayBuffer} data
  39. * @param {!string} uri
  40. * @return {!shaka.hls.Playlist}
  41. * @throws {shaka.util.Error}
  42. */
  43. shaka.hls.ManifestTextParser.prototype.parsePlaylist = function(data, uri) {
  44. // Get the input as a string. Normalize newlines to \n.
  45. var str = shaka.util.StringUtils.fromUTF8(data);
  46. str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n').trim();
  47. var lines = str.split(/\n+/m);
  48. if (!/^#EXTM3U($|[ \t\n])/m.test(lines[0])) {
  49. throw new shaka.util.Error(
  50. shaka.util.Error.Severity.CRITICAL,
  51. shaka.util.Error.Category.MANIFEST,
  52. shaka.util.Error.Code.HLS_PLAYLIST_HEADER_MISSING);
  53. }
  54. /** shaka.hls.PlaylistType */
  55. var playlistType = shaka.hls.PlaylistType.MASTER;
  56. /** {Array.<shaka.hls.Tag>} */
  57. var tags = [];
  58. var i = 1;
  59. while (i < lines.length) {
  60. // Skip comments
  61. if (shaka.hls.Utils.isComment(lines[i])) {
  62. i += 1;
  63. continue;
  64. }
  65. var tag = this.parseTag_(lines[i]);
  66. if (shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS
  67. .indexOf(tag.name) >= 0) {
  68. playlistType = shaka.hls.PlaylistType.MEDIA;
  69. } else if (shaka.hls.ManifestTextParser.SEGMENT_TAGS
  70. .indexOf(tag.name) >= 0) {
  71. if (playlistType != shaka.hls.PlaylistType.MEDIA) {
  72. // Only media playlist should contain segment tags
  73. throw new shaka.util.Error(
  74. shaka.util.Error.Severity.CRITICAL,
  75. shaka.util.Error.Category.MANIFEST,
  76. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  77. }
  78. var segmentsData = lines.splice(i, lines.length - i);
  79. var segments = this.parseSegments_(segmentsData, tags);
  80. return new shaka.hls.Playlist(uri, playlistType, tags, segments);
  81. }
  82. tags.push(tag);
  83. i += 1;
  84. // EXT-X-STREAM-INF tag is followed by a uri of a media playlist.
  85. // Add uri to the tag object.
  86. if (tag.name == 'EXT-X-STREAM-INF') {
  87. var tagUri = new shaka.hls.Attribute('URI', lines[i]);
  88. tag.addAttribute(tagUri);
  89. i += 1;
  90. }
  91. }
  92. return new shaka.hls.Playlist(uri, playlistType, tags);
  93. };
  94. /**
  95. * Parses an array of strings into an HLS Segment objects.
  96. *
  97. * @param {!Array.<string>} lines
  98. * @param {!Array.<!shaka.hls.Tag>} playlistTags
  99. * @return {!Array.<shaka.hls.Segment>}
  100. * @private
  101. * @throws {shaka.util.Error}
  102. */
  103. shaka.hls.ManifestTextParser.prototype.parseSegments_ =
  104. function(lines, playlistTags) {
  105. /** @type {!Array.<shaka.hls.Segment>} */
  106. var segments = [];
  107. /** @type {!Array.<shaka.hls.Tag>} */
  108. var segmentTags = [];
  109. lines.forEach(function(line) {
  110. if (/^(#EXT)/.test(line)) {
  111. var tag = this.parseTag_(line);
  112. if (shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS
  113. .indexOf(tag.name) >= 0) {
  114. playlistTags.push(tag);
  115. } else {
  116. segmentTags.push(tag);
  117. }
  118. } else if (shaka.hls.Utils.isComment(line)) {
  119. // Skip comments
  120. return;
  121. } else {
  122. var uri = line.trim();
  123. // Uri appears after all the tags describing the segment.
  124. var segment = new shaka.hls.Segment(uri, segmentTags);
  125. segments.push(segment);
  126. segmentTags = [];
  127. }
  128. }.bind(this));
  129. return segments;
  130. };
  131. /**
  132. * Parses a string into an HLS Tag object while tracking what id to use next.
  133. *
  134. * @param {!string} word
  135. * @return {!shaka.hls.Tag}
  136. * @throws {shaka.util.Error}
  137. * @private
  138. */
  139. shaka.hls.ManifestTextParser.prototype.parseTag_ = function(word) {
  140. return shaka.hls.ManifestTextParser.parseTag(this.globalId_++, word);
  141. };
  142. /**
  143. * Parses a string into an HLS Tag object.
  144. *
  145. * @param {number} id
  146. * @param {!string} word
  147. * @return {!shaka.hls.Tag}
  148. * @throws {shaka.util.Error}
  149. */
  150. shaka.hls.ManifestTextParser.parseTag = function(id, word) {
  151. /* HLS tags start with '#EXT'. A tag can have a set of attributes
  152. (#EXT-<tagname>:<attribute list>) or a value (#EXT-<tagname>:<value>).
  153. Attributes' format is 'AttributeName=AttributeValue'.
  154. The parsing logic goes like this:
  155. 1) Everything before ':' is a name (we ignore '#').
  156. 2) Everything after should be parsed as attributes if it contains '='.
  157. 3) Otherwise, this is a value.
  158. 4) If there is no ":", it's a simple tag with no attributes and no value */
  159. var blocks = word.match(/^#(EXT[^:]*)(?::(.*))?$/);
  160. if (!blocks) {
  161. throw new shaka.util.Error(
  162. shaka.util.Error.Severity.CRITICAL,
  163. shaka.util.Error.Category.MANIFEST,
  164. shaka.util.Error.Code.INVALID_HLS_TAG);
  165. }
  166. var name = blocks[1];
  167. var data = blocks[2];
  168. var attributes = [];
  169. if (data && data.indexOf('=') >= 0) {
  170. var parser = new shaka.util.TextParser(data);
  171. var blockAttrs;
  172. // Regex:
  173. // 1. Key name ([1])
  174. // 2. Equals sign
  175. // 3. Either:
  176. // a. A quoted string (everything up to the next quote, [2])
  177. // b. An unquoted string
  178. // (everything up to the next comma or end of line, [3])
  179. // 4. Either:
  180. // a. A comma
  181. // b. End of line
  182. var regex = /([^=]+)=(?:"([^"]*)"|([^",]*))(?:,|$)/g;
  183. while (blockAttrs = parser.readRegex(regex)) {
  184. var attrName = blockAttrs[1];
  185. var attrValue = blockAttrs[2] || blockAttrs[3];
  186. var attribute = new shaka.hls.Attribute(attrName, attrValue);
  187. attributes.push(attribute);
  188. }
  189. } else if (data) {
  190. return new shaka.hls.Tag(id, name, attributes, data);
  191. }
  192. return new shaka.hls.Tag(id, name, attributes);
  193. };
  194. /**
  195. * HLS tags that only appear on Media Playlists.
  196. * Used to determine a playlist type.
  197. *
  198. * @const {!Array<!string>}
  199. */
  200. shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [
  201. 'EXT-X-TARGETDURATION',
  202. 'EXT-X-MEDIA-SEQUENCE',
  203. 'EXT-X-DISCONTINUITY-SEQUENCE',
  204. 'EXT-X-PLAYLIST-TYPE',
  205. 'EXT-X-MAP',
  206. 'EXT-X-I-FRAMES-ONLY',
  207. 'EXT-X-ENDLIST'
  208. ];
  209. /**
  210. * HLS tags that only appear on Segments in a Media Playlists.
  211. * Used to determine the start of the segments info.
  212. *
  213. * @const {!Array<!string>}
  214. */
  215. shaka.hls.ManifestTextParser.SEGMENT_TAGS = [
  216. 'EXTINF',
  217. 'EXT-X-BYTERANGE',
  218. 'EXT-X-DISCONTINUITY',
  219. 'EXT-X-PROGRAM-DATE-TIME',
  220. 'EXT-X-KEY',
  221. 'EXT-X-DATERANGE'
  222. ];