Source: lib/media/webm_segment_index_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.media.WebmSegmentIndexParser');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.media.SegmentReference');
  21. goog.require('shaka.util.EbmlElement');
  22. goog.require('shaka.util.EbmlParser');
  23. goog.require('shaka.util.Error');
  24. /**
  25. * Creates a WebM Cues element parser.
  26. *
  27. * @constructor
  28. * @struct
  29. */
  30. shaka.media.WebmSegmentIndexParser = function() {};
  31. /** @const {number} */
  32. shaka.media.WebmSegmentIndexParser.EBML_ID = 0x1a45dfa3;
  33. /** @const {number} */
  34. shaka.media.WebmSegmentIndexParser.SEGMENT_ID = 0x18538067;
  35. /** @const {number} */
  36. shaka.media.WebmSegmentIndexParser.INFO_ID = 0x1549a966;
  37. /** @const {number} */
  38. shaka.media.WebmSegmentIndexParser.TIMECODE_SCALE_ID = 0x2ad7b1;
  39. /** @const {number} */
  40. shaka.media.WebmSegmentIndexParser.DURATION_ID = 0x4489;
  41. /** @const {number} */
  42. shaka.media.WebmSegmentIndexParser.CUES_ID = 0x1c53bb6b;
  43. /** @const {number} */
  44. shaka.media.WebmSegmentIndexParser.CUE_POINT_ID = 0xbb;
  45. /** @const {number} */
  46. shaka.media.WebmSegmentIndexParser.CUE_TIME_ID = 0xb3;
  47. /** @const {number} */
  48. shaka.media.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID = 0xb7;
  49. /** @const {number} */
  50. shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1;
  51. /**
  52. * Parses SegmentReferences from a WebM container.
  53. * @param {!ArrayBuffer} cuesData The WebM container's "Cueing Data" section.
  54. * @param {!ArrayBuffer} initData The WebM container's headers.
  55. * @param {!Array.<string>} uris The possible locations of the WebM file that
  56. * contains the segments.
  57. * @param {number} scaledPresentationTimeOffset
  58. * @return {!Array.<!shaka.media.SegmentReference>}
  59. * @throws {shaka.util.Error}
  60. * @see http://www.matroska.org/technical/specs/index.html
  61. * @see http://www.webmproject.org/docs/container/
  62. */
  63. shaka.media.WebmSegmentIndexParser.prototype.parse = function(
  64. cuesData, initData, uris, scaledPresentationTimeOffset) {
  65. var tuple = this.parseWebmContainer_(initData);
  66. var parser = new shaka.util.EbmlParser(new DataView(cuesData));
  67. var cuesElement = parser.parseElement();
  68. if (cuesElement.id != shaka.media.WebmSegmentIndexParser.CUES_ID) {
  69. shaka.log.error('Not a Cues element.');
  70. throw new shaka.util.Error(
  71. shaka.util.Error.Severity.CRITICAL,
  72. shaka.util.Error.Category.MEDIA,
  73. shaka.util.Error.Code.WEBM_CUES_ELEMENT_MISSING);
  74. }
  75. return this.parseCues_(
  76. cuesElement, tuple.segmentOffset, tuple.timecodeScale, tuple.duration,
  77. uris, scaledPresentationTimeOffset);
  78. };
  79. /**
  80. * Parses a WebM container to get the segment's offset, timecode scale, and
  81. * duration.
  82. *
  83. * @param {!ArrayBuffer} initData
  84. * @return {{segmentOffset: number, timecodeScale: number, duration: number}}
  85. * The segment's offset in bytes, the segment's timecode scale in seconds,
  86. * and the duration in seconds.
  87. * @throws {shaka.util.Error}
  88. * @private
  89. */
  90. shaka.media.WebmSegmentIndexParser.prototype.parseWebmContainer_ = function(
  91. initData) {
  92. var parser = new shaka.util.EbmlParser(new DataView(initData));
  93. // Check that the WebM container data starts with the EBML header, but
  94. // skip its contents.
  95. var ebmlElement = parser.parseElement();
  96. if (ebmlElement.id != shaka.media.WebmSegmentIndexParser.EBML_ID) {
  97. shaka.log.error('Not an EBML element.');
  98. throw new shaka.util.Error(
  99. shaka.util.Error.Severity.CRITICAL,
  100. shaka.util.Error.Category.MEDIA,
  101. shaka.util.Error.Code.WEBM_EBML_HEADER_ELEMENT_MISSING);
  102. }
  103. var segmentElement = parser.parseElement();
  104. if (segmentElement.id != shaka.media.WebmSegmentIndexParser.SEGMENT_ID) {
  105. shaka.log.error('Not a Segment element.');
  106. throw new shaka.util.Error(
  107. shaka.util.Error.Severity.CRITICAL,
  108. shaka.util.Error.Category.MEDIA,
  109. shaka.util.Error.Code.WEBM_SEGMENT_ELEMENT_MISSING);
  110. }
  111. // This value is used as the initial offset to the first referenced segment.
  112. var segmentOffset = segmentElement.getOffset();
  113. // Parse the Segment element to get the segment info.
  114. var segmentInfo = this.parseSegment_(segmentElement);
  115. return {
  116. segmentOffset: segmentOffset,
  117. timecodeScale: segmentInfo.timecodeScale,
  118. duration: segmentInfo.duration
  119. };
  120. };
  121. /**
  122. * Parses a WebM Info element to get the segment's timecode scale and duration.
  123. * @param {!shaka.util.EbmlElement} segmentElement
  124. * @return {{timecodeScale: number, duration: number}} The segment's timecode
  125. * scale in seconds and duration in seconds.
  126. * @throws {shaka.util.Error}
  127. * @private
  128. */
  129. shaka.media.WebmSegmentIndexParser.prototype.parseSegment_ = function(
  130. segmentElement) {
  131. var parser = segmentElement.createParser();
  132. // Find the Info element.
  133. var infoElement = null;
  134. while (parser.hasMoreData()) {
  135. var elem = parser.parseElement();
  136. if (elem.id != shaka.media.WebmSegmentIndexParser.INFO_ID) {
  137. continue;
  138. }
  139. infoElement = elem;
  140. break;
  141. }
  142. if (!infoElement) {
  143. shaka.log.error('Not an Info element.');
  144. throw new shaka.util.Error(
  145. shaka.util.Error.Severity.CRITICAL,
  146. shaka.util.Error.Category.MEDIA,
  147. shaka.util.Error.Code.WEBM_INFO_ELEMENT_MISSING);
  148. }
  149. return this.parseInfo_(infoElement);
  150. };
  151. /**
  152. * Parses a WebM Info element to get the segment's timecode scale and duration.
  153. * @param {!shaka.util.EbmlElement} infoElement
  154. * @return {{timecodeScale: number, duration: number}} The segment's timecode
  155. * scale in seconds and duration in seconds.
  156. * @throws {shaka.util.Error}
  157. * @private
  158. */
  159. shaka.media.WebmSegmentIndexParser.prototype.parseInfo_ = function(
  160. infoElement) {
  161. var parser = infoElement.createParser();
  162. // The timecode scale factor in units of [nanoseconds / T], where [T] are the
  163. // units used to express all other time values in the WebM container. By
  164. // default it's assumed that [T] == [milliseconds].
  165. var timecodeScaleNanoseconds = 1000000;
  166. /** @type {?number} */
  167. var durationScale = null;
  168. while (parser.hasMoreData()) {
  169. var elem = parser.parseElement();
  170. if (elem.id == shaka.media.WebmSegmentIndexParser.TIMECODE_SCALE_ID) {
  171. timecodeScaleNanoseconds = elem.getUint();
  172. } else if (elem.id == shaka.media.WebmSegmentIndexParser.DURATION_ID) {
  173. durationScale = elem.getFloat();
  174. }
  175. }
  176. if (durationScale == null) {
  177. throw new shaka.util.Error(
  178. shaka.util.Error.Severity.CRITICAL,
  179. shaka.util.Error.Category.MEDIA,
  180. shaka.util.Error.Code.WEBM_DURATION_ELEMENT_MISSING);
  181. }
  182. // The timecode scale factor in units of [seconds / T].
  183. var timecodeScale = timecodeScaleNanoseconds / 1000000000;
  184. // The duration is stored in units of [T]
  185. var durationSeconds = durationScale * timecodeScale;
  186. return {timecodeScale: timecodeScale, duration: durationSeconds};
  187. };
  188. /**
  189. * Parses a WebM CuesElement.
  190. * @param {!shaka.util.EbmlElement} cuesElement
  191. * @param {number} segmentOffset
  192. * @param {number} timecodeScale
  193. * @param {number} duration
  194. * @param {!Array.<string>} uris
  195. * @param {number} scaledPresentationTimeOffset
  196. * @return {!Array.<!shaka.media.SegmentReference>}
  197. * @throws {shaka.util.Error}
  198. * @private
  199. */
  200. shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function(
  201. cuesElement, segmentOffset, timecodeScale, duration, uris,
  202. scaledPresentationTimeOffset) {
  203. var references = [];
  204. var getUris = function() { return uris; };
  205. var parser = cuesElement.createParser();
  206. var lastTime = null;
  207. var lastOffset = null;
  208. while (parser.hasMoreData()) {
  209. var elem = parser.parseElement();
  210. if (elem.id != shaka.media.WebmSegmentIndexParser.CUE_POINT_ID) {
  211. continue;
  212. }
  213. var tuple = this.parseCuePoint_(elem);
  214. if (!tuple) {
  215. continue;
  216. }
  217. // Substract presentation time offset from unscaled time
  218. var currentTime = timecodeScale * tuple.unscaledTime;
  219. var currentOffset = segmentOffset + tuple.relativeOffset;
  220. if (lastTime != null) {
  221. goog.asserts.assert(lastOffset != null, 'last offset cannot be null');
  222. references.push(
  223. new shaka.media.SegmentReference(
  224. references.length,
  225. lastTime - scaledPresentationTimeOffset,
  226. currentTime - scaledPresentationTimeOffset,
  227. getUris,
  228. lastOffset, currentOffset - 1));
  229. }
  230. lastTime = currentTime;
  231. lastOffset = currentOffset;
  232. }
  233. if (lastTime != null) {
  234. goog.asserts.assert(lastOffset != null, 'last offset cannot be null');
  235. references.push(
  236. new shaka.media.SegmentReference(
  237. references.length,
  238. lastTime - scaledPresentationTimeOffset,
  239. duration - scaledPresentationTimeOffset,
  240. getUris,
  241. lastOffset, null));
  242. }
  243. return references;
  244. };
  245. /**
  246. * Parses a WebM CuePointElement to get an "unadjusted" segment reference.
  247. * @param {shaka.util.EbmlElement} cuePointElement
  248. * @return {{unscaledTime: number, relativeOffset: number}} The referenced
  249. * segment's start time in units of [T] (see parseInfo_()), and the
  250. * referenced segment's offset in bytes, relative to a WebM Segment
  251. * element.
  252. * @throws {shaka.util.Error}
  253. * @private
  254. */
  255. shaka.media.WebmSegmentIndexParser.prototype.parseCuePoint_ = function(
  256. cuePointElement) {
  257. var parser = cuePointElement.createParser();
  258. // Parse CueTime element.
  259. var cueTimeElement = parser.parseElement();
  260. if (cueTimeElement.id != shaka.media.WebmSegmentIndexParser.CUE_TIME_ID) {
  261. shaka.log.warning('Not a CueTime element.');
  262. throw new shaka.util.Error(
  263. shaka.util.Error.Severity.CRITICAL,
  264. shaka.util.Error.Category.MEDIA,
  265. shaka.util.Error.Code.WEBM_CUE_TIME_ELEMENT_MISSING);
  266. }
  267. var unscaledTime = cueTimeElement.getUint();
  268. // Parse CueTrackPositions element.
  269. var cueTrackPositionsElement = parser.parseElement();
  270. if (cueTrackPositionsElement.id !=
  271. shaka.media.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID) {
  272. shaka.log.warning('Not a CueTrackPositions element.');
  273. throw new shaka.util.Error(
  274. shaka.util.Error.Severity.CRITICAL,
  275. shaka.util.Error.Category.MEDIA,
  276. shaka.util.Error.Code.WEBM_CUE_TRACK_POSITIONS_ELEMENT_MISSING);
  277. }
  278. var cueTrackParser = cueTrackPositionsElement.createParser();
  279. var relativeOffset = 0;
  280. while (cueTrackParser.hasMoreData()) {
  281. var elem = cueTrackParser.parseElement();
  282. if (elem.id != shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION) {
  283. continue;
  284. }
  285. relativeOffset = elem.getUint();
  286. break;
  287. }
  288. return { unscaledTime: unscaledTime, relativeOffset: relativeOffset };
  289. };