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} absolutePlaylistUri An absolute, final URI after redirects.
  40. * @return {!shaka.hls.Playlist}
  41. * @throws {shaka.util.Error}
  42. */
  43. shaka.hls.ManifestTextParser.prototype.parsePlaylist =
  44. function(data, absolutePlaylistUri) {
  45. const MEDIA_PLAYLIST_TAGS = shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS;
  46. const SEGMENT_TAGS = shaka.hls.ManifestTextParser.SEGMENT_TAGS;
  47. // Get the input as a string. Normalize newlines to \n.
  48. let str = shaka.util.StringUtils.fromUTF8(data);
  49. str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n').trim();
  50. const lines = str.split(/\n+/m);
  51. if (!/^#EXTM3U($|[ \t\n])/m.test(lines[0])) {
  52. throw new shaka.util.Error(
  53. shaka.util.Error.Severity.CRITICAL,
  54. shaka.util.Error.Category.MANIFEST,
  55. shaka.util.Error.Code.HLS_PLAYLIST_HEADER_MISSING);
  56. }
  57. /** shaka.hls.PlaylistType */
  58. let playlistType = shaka.hls.PlaylistType.MASTER;
  59. // First, look for media playlist tags, so that we know what the playlist
  60. // type really is before we start parsing.
  61. for (let i = 1; i < lines.length; i++) {
  62. // Ignore comments.
  63. if (!shaka.hls.Utils.isComment(lines[i])) {
  64. const tag = this.parseTag_(lines[i]);
  65. // These tags won't actually be used, so don't increment the global id.
  66. this.globalId_ -= 1;
  67. if (MEDIA_PLAYLIST_TAGS.includes(tag.name)) {
  68. playlistType = shaka.hls.PlaylistType.MEDIA;
  69. break;
  70. } else if (tag.name == 'EXT-X-STREAM-INF') {
  71. i += 1;
  72. }
  73. }
  74. }
  75. /** {Array.<shaka.hls.Tag>} */
  76. const tags = [];
  77. for (let i = 1; i < lines.length;) {
  78. // Skip comments
  79. if (shaka.hls.Utils.isComment(lines[i])) {
  80. i += 1;
  81. continue;
  82. }
  83. const tag = this.parseTag_(lines[i]);
  84. if (SEGMENT_TAGS.includes(tag.name)) {
  85. if (playlistType != shaka.hls.PlaylistType.MEDIA) {
  86. // Only media playlists should contain segment tags
  87. throw new shaka.util.Error(
  88. shaka.util.Error.Severity.CRITICAL,
  89. shaka.util.Error.Category.MANIFEST,
  90. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  91. }
  92. const segmentsData = lines.splice(i, lines.length - i);
  93. const segments = this.parseSegments_(
  94. absolutePlaylistUri, segmentsData, tags);
  95. return new shaka.hls.Playlist(
  96. absolutePlaylistUri, playlistType, tags, segments);
  97. }
  98. tags.push(tag);
  99. i += 1;
  100. // An EXT-X-STREAM-INF tag is followed by a URI of a media playlist.
  101. // Add the URI to the tag object.
  102. if (tag.name == 'EXT-X-STREAM-INF') {
  103. const tagUri = new shaka.hls.Attribute('URI', lines[i]);
  104. tag.addAttribute(tagUri);
  105. i += 1;
  106. }
  107. }
  108. return new shaka.hls.Playlist(absolutePlaylistUri, playlistType, tags);
  109. };
  110. /**
  111. * Parses an array of strings into an array of HLS Segment objects.
  112. *
  113. * @param {string} absoluteMediaPlaylistUri
  114. * @param {!Array.<string>} lines
  115. * @param {!Array.<!shaka.hls.Tag>} playlistTags
  116. * @return {!Array.<shaka.hls.Segment>}
  117. * @private
  118. * @throws {shaka.util.Error}
  119. */
  120. shaka.hls.ManifestTextParser.prototype.parseSegments_ =
  121. function(absoluteMediaPlaylistUri, lines, playlistTags) {
  122. /** @type {!Array.<shaka.hls.Segment>} */
  123. let segments = [];
  124. /** @type {!Array.<shaka.hls.Tag>} */
  125. let segmentTags = [];
  126. lines.forEach((line) => {
  127. if (/^(#EXT)/.test(line)) {
  128. let tag = this.parseTag_(line);
  129. if (shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS.includes(tag.name)) {
  130. playlistTags.push(tag);
  131. } else {
  132. segmentTags.push(tag);
  133. }
  134. } else if (shaka.hls.Utils.isComment(line)) {
  135. // Skip comments.
  136. return [];
  137. } else {
  138. const verbatimSegmentUri = line.trim();
  139. const absoluteSegmentUri = shaka.hls.Utils.constructAbsoluteUri(
  140. absoluteMediaPlaylistUri, verbatimSegmentUri);
  141. // The URI appears after all of the tags describing the segment.
  142. const segment = new shaka.hls.Segment(absoluteSegmentUri, segmentTags);
  143. segments.push(segment);
  144. segmentTags = [];
  145. }
  146. });
  147. return segments;
  148. };
  149. /**
  150. * Parses a string into an HLS Tag object while tracking what id to use next.
  151. *
  152. * @param {string} word
  153. * @return {!shaka.hls.Tag}
  154. * @throws {shaka.util.Error}
  155. * @private
  156. */
  157. shaka.hls.ManifestTextParser.prototype.parseTag_ = function(word) {
  158. return shaka.hls.ManifestTextParser.parseTag(this.globalId_++, word);
  159. };
  160. /**
  161. * Parses a string into an HLS Tag object.
  162. *
  163. * @param {number} id
  164. * @param {string} word
  165. * @return {!shaka.hls.Tag}
  166. * @throws {shaka.util.Error}
  167. */
  168. shaka.hls.ManifestTextParser.parseTag = function(id, word) {
  169. /* HLS tags start with '#EXT'. A tag can have a set of attributes
  170. (#EXT-<tagname>:<attribute list>) and/or a value (#EXT-<tagname>:<value>).
  171. An attribute's format is 'AttributeName=AttributeValue'.
  172. The parsing logic goes like this:
  173. 1. Everything before ':' is a name (we ignore '#').
  174. 2. Everything after ':' is a list of comma-seprated items,
  175. 2a. The first item might be a value, if it does not contain '='.
  176. 2b. Otherwise, items are attributes.
  177. 3. If there is no ":", it's a simple tag with no attributes and no value */
  178. const blocks = word.match(/^#(EXT[^:]*)(?::(.*))?$/);
  179. if (!blocks) {
  180. throw new shaka.util.Error(
  181. shaka.util.Error.Severity.CRITICAL,
  182. shaka.util.Error.Category.MANIFEST,
  183. shaka.util.Error.Code.INVALID_HLS_TAG,
  184. word);
  185. }
  186. const name = blocks[1];
  187. const data = blocks[2];
  188. const attributes = [];
  189. let value;
  190. if (data) {
  191. const parser = new shaka.util.TextParser(data);
  192. let blockAttrs;
  193. // Regex: any number of non-equals-sign characters at the beginning
  194. // terminated by comma or end of line
  195. const valueRegex = /^([^,=]+)(?:,|$)/g;
  196. const blockValue = parser.readRegex(valueRegex);
  197. if (blockValue) {
  198. value = blockValue[1];
  199. }
  200. // Regex:
  201. // 1. Key name ([1])
  202. // 2. Equals sign
  203. // 3. Either:
  204. // a. A quoted string (everything up to the next quote, [2])
  205. // b. An unquoted string
  206. // (everything up to the next comma or end of line, [3])
  207. // 4. Either:
  208. // a. A comma
  209. // b. End of line
  210. const attributeRegex = /([^=]+)=(?:"([^"]*)"|([^",]*))(?:,|$)/g;
  211. while ((blockAttrs = parser.readRegex(attributeRegex))) {
  212. const attrName = blockAttrs[1];
  213. const attrValue = blockAttrs[2] || blockAttrs[3];
  214. const attribute = new shaka.hls.Attribute(attrName, attrValue);
  215. attributes.push(attribute);
  216. }
  217. }
  218. return new shaka.hls.Tag(id, name, attributes, value);
  219. };
  220. /**
  221. * HLS tags that only appear on Media Playlists.
  222. * Used to determine a playlist type.
  223. *
  224. * @const {!Array.<string>}
  225. */
  226. shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [
  227. 'EXT-X-TARGETDURATION',
  228. 'EXT-X-MEDIA-SEQUENCE',
  229. 'EXT-X-DISCONTINUITY-SEQUENCE',
  230. 'EXT-X-PLAYLIST-TYPE',
  231. 'EXT-X-MAP',
  232. 'EXT-X-I-FRAMES-ONLY',
  233. 'EXT-X-ENDLIST',
  234. ];
  235. /**
  236. * HLS tags that only appear on Segments in a Media Playlists.
  237. * Used to determine the start of the segments info.
  238. *
  239. * @const {!Array.<string>}
  240. */
  241. shaka.hls.ManifestTextParser.SEGMENT_TAGS = [
  242. 'EXTINF',
  243. 'EXT-X-BYTERANGE',
  244. 'EXT-X-DISCONTINUITY',
  245. 'EXT-X-PROGRAM-DATE-TIME',
  246. 'EXT-X-KEY',
  247. 'EXT-X-DATERANGE',
  248. ];