Source: lib/dash/segment_list.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.dash.SegmentList');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.dash.MpdUtils');
  20. goog.require('shaka.dash.SegmentBase');
  21. goog.require('shaka.media.SegmentIndex');
  22. goog.require('shaka.media.SegmentReference');
  23. goog.require('shaka.util.Error');
  24. goog.require('shaka.util.Functional');
  25. goog.require('shaka.util.XmlUtils');
  26. /**
  27. * @namespace shaka.dash.SegmentList
  28. * @summary A set of functions for parsing SegmentList elements.
  29. */
  30. /**
  31. * Creates a new Stream object or updates the Stream in the manifest.
  32. *
  33. * @param {shaka.dash.DashParser.Context} context
  34. * @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
  35. * @param {?shakaExtern.Manifest} manifest
  36. * @return {shaka.dash.DashParser.StreamInfo}
  37. */
  38. shaka.dash.SegmentList.createStream = function(
  39. context, segmentIndexMap, manifest) {
  40. goog.asserts.assert(context.representation.segmentList,
  41. 'Should only be called with SegmentList');
  42. var SegmentList = shaka.dash.SegmentList;
  43. var init = shaka.dash.SegmentBase.createInitSegment(
  44. context, SegmentList.fromInheritance_);
  45. var info = SegmentList.parseSegmentListInfo_(context);
  46. SegmentList.checkSegmentListInfo_(context, info);
  47. /** @type {shaka.media.SegmentIndex} */
  48. var segmentIndex = null;
  49. var id = null;
  50. if (context.period.id && context.representation.id) {
  51. // Only check/store the index if period and representation IDs are set.
  52. id = context.period.id + ',' + context.representation.id;
  53. segmentIndex = segmentIndexMap[id];
  54. }
  55. var references = SegmentList.createSegmentReferences_(
  56. context.periodInfo.start, context.periodInfo.duration, info.startNumber,
  57. context.representation.baseUris, info);
  58. shaka.dash.MpdUtils.fitSegmentReferences(
  59. context.periodInfo.duration, references);
  60. if (segmentIndex) {
  61. goog.asserts.assert(manifest, 'This should be an update');
  62. segmentIndex.merge(references);
  63. segmentIndex.evict(
  64. manifest.presentationTimeline.getSegmentAvailabilityStart());
  65. } else {
  66. context.maxSegmentDuration = references.reduce(
  67. function(max, r) { return Math.max(max, r.endTime - r.startTime); },
  68. context.maxSegmentDuration);
  69. segmentIndex = new shaka.media.SegmentIndex(references);
  70. if (id)
  71. segmentIndexMap[id] = segmentIndex;
  72. }
  73. return {
  74. createSegmentIndex: Promise.resolve.bind(Promise),
  75. findSegmentPosition: segmentIndex.find.bind(segmentIndex),
  76. getSegmentReference: segmentIndex.get.bind(segmentIndex),
  77. initSegmentReference: init,
  78. presentationTimeOffset: info.presentationTimeOffset
  79. };
  80. };
  81. /**
  82. * @typedef {{
  83. * mediaUri: string,
  84. * start: number,
  85. * end: ?number
  86. * }}
  87. *
  88. * @property {string} mediaUri
  89. * The URI of the segment.
  90. * @property {number} start
  91. * The start byte of the segment.
  92. * @property {?number} end
  93. * The end byte of the segment, or null.
  94. */
  95. shaka.dash.SegmentList.MediaSegment;
  96. /**
  97. * @typedef {{
  98. * segmentDuration: ?number,
  99. * startTime: number,
  100. * startNumber: number,
  101. * presentationTimeOffset: number,
  102. * timeline: Array.<shaka.dash.MpdUtils.TimeRange>,
  103. * mediaSegments: !Array.<shaka.dash.SegmentList.MediaSegment>
  104. * }}
  105. * @private
  106. *
  107. * @description
  108. * Contains information about a SegmentList.
  109. *
  110. * @property {?number} segmentDuration
  111. * The duration of the segments, if given.
  112. * @property {number} startTime
  113. * The start time of the first segment, in seconds.
  114. * @property {number} startNumber
  115. * The start number of the segments; 1 or greater.
  116. * @property {number} presentationTimeOffset
  117. * The presentationTimeOffset of the representation, in seconds.
  118. * @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
  119. * The timeline of the representation, if given. Times in seconds.
  120. * @property {!Array.<shaka.dash.SegmentList.MediaSegment>} mediaSegments
  121. * The URI and byte-ranges of the media segments.
  122. */
  123. shaka.dash.SegmentList.SegmentListInfo;
  124. /**
  125. * @param {?shaka.dash.DashParser.InheritanceFrame} frame
  126. * @return {Element}
  127. * @private
  128. */
  129. shaka.dash.SegmentList.fromInheritance_ = function(frame) {
  130. return frame.segmentList;
  131. };
  132. /**
  133. * Parses the SegmentList items to create an info object.
  134. *
  135. * @param {shaka.dash.DashParser.Context} context
  136. * @return {shaka.dash.SegmentList.SegmentListInfo}
  137. * @private
  138. */
  139. shaka.dash.SegmentList.parseSegmentListInfo_ = function(context) {
  140. var SegmentList = shaka.dash.SegmentList;
  141. var MpdUtils = shaka.dash.MpdUtils;
  142. var mediaSegments = SegmentList.parseMediaSegments_(context);
  143. var segmentInfo =
  144. MpdUtils.parseSegmentInfo(context, SegmentList.fromInheritance_);
  145. var startNumber = segmentInfo.startNumber;
  146. if (startNumber === 0) {
  147. shaka.log.warning('SegmentList@startNumber must be > 0');
  148. startNumber = 1;
  149. }
  150. var startTime = 0;
  151. if (segmentInfo.segmentDuration) {
  152. // Consider the presentationTimeOffset
  153. startTime = segmentInfo.segmentDuration * (startNumber - 1) -
  154. segmentInfo.presentationTimeOffset;
  155. } else if (segmentInfo.timeline && segmentInfo.timeline.length > 0) {
  156. // The presentationTimeOffset was considered in timeline creation
  157. startTime = segmentInfo.timeline[0].start;
  158. }
  159. return {
  160. segmentDuration: segmentInfo.segmentDuration,
  161. startTime: startTime,
  162. startNumber: startNumber,
  163. presentationTimeOffset: segmentInfo.presentationTimeOffset,
  164. timeline: segmentInfo.timeline,
  165. mediaSegments: mediaSegments
  166. };
  167. };
  168. /**
  169. * Checks whether a SegmentListInfo object is valid.
  170. *
  171. * @param {shaka.dash.DashParser.Context} context
  172. * @param {shaka.dash.SegmentList.SegmentListInfo} info
  173. * @throws shaka.util.Error When there is a parsing error.
  174. * @private
  175. */
  176. shaka.dash.SegmentList.checkSegmentListInfo_ = function(context, info) {
  177. if (!info.segmentDuration && !info.timeline &&
  178. info.mediaSegments.length > 1) {
  179. shaka.log.warning(
  180. 'SegmentList does not contain sufficient segment information:',
  181. 'the SegmentList specifies multiple segments,',
  182. 'but does not specify a segment duration or timeline.',
  183. context.representation);
  184. throw new shaka.util.Error(
  185. shaka.util.Error.Category.MANIFEST,
  186. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  187. }
  188. if (!info.segmentDuration && !context.periodInfo.duration && !info.timeline &&
  189. info.mediaSegments.length == 1) {
  190. shaka.log.warning(
  191. 'SegmentList does not contain sufficient segment information:',
  192. 'the SegmentList specifies one segment,',
  193. 'but does not specify a segment duration, period duration,',
  194. 'or timeline.',
  195. context.representation);
  196. throw new shaka.util.Error(
  197. shaka.util.Error.Category.MANIFEST,
  198. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  199. }
  200. if (info.timeline && info.timeline.length == 0) {
  201. shaka.log.warning(
  202. 'SegmentList does not contain sufficient segment information:',
  203. 'the SegmentList has an empty timeline.',
  204. context.representation);
  205. throw new shaka.util.Error(
  206. shaka.util.Error.Category.MANIFEST,
  207. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  208. }
  209. };
  210. /**
  211. * Creates an array of segment references for the given data.
  212. *
  213. * @param {number} periodStart in seconds.
  214. * @param {?number} periodDuration in seconds.
  215. * @param {number} startNumber
  216. * @param {!Array.<string>} baseUris
  217. * @param {shaka.dash.SegmentList.SegmentListInfo} info
  218. * @return {!Array.<!shaka.media.SegmentReference>}
  219. * @private
  220. */
  221. shaka.dash.SegmentList.createSegmentReferences_ = function(
  222. periodStart, periodDuration, startNumber, baseUris, info) {
  223. var MpdUtils = shaka.dash.MpdUtils;
  224. var max = info.mediaSegments.length;
  225. if (info.timeline && info.timeline.length != info.mediaSegments.length) {
  226. max = Math.min(info.timeline.length, info.mediaSegments.length);
  227. shaka.log.warning(
  228. 'The number of items in the segment timeline and the number of segment',
  229. 'URLs do not match, truncating', info.mediaSegments.length, 'to', max);
  230. }
  231. /** @type {!Array.<!shaka.media.SegmentReference>} */
  232. var references = [];
  233. var prevEndTime = info.startTime;
  234. for (var i = 0; i < max; i++) {
  235. var segment = info.mediaSegments[i];
  236. var mediaUri = MpdUtils.resolveUris(baseUris, [segment.mediaUri]);
  237. var startTime = prevEndTime;
  238. var endTime;
  239. if (info.segmentDuration != null) {
  240. endTime = startTime + info.segmentDuration;
  241. } else if (info.timeline) {
  242. // Ignore the timepoint start since they are continuous.
  243. endTime = info.timeline[i].end;
  244. } else {
  245. // If segmentDuration and timeline are null then there must
  246. // only be one segment.
  247. goog.asserts.assert(
  248. info.mediaSegments.length == 1 && periodDuration,
  249. 'There should only be one segment with a Period duration.');
  250. endTime = startTime + periodDuration;
  251. }
  252. references.push(
  253. new shaka.media.SegmentReference(
  254. i + startNumber, startTime, endTime, mediaUri, segment.start,
  255. segment.end));
  256. prevEndTime = endTime;
  257. }
  258. return references;
  259. };
  260. /**
  261. * Parses the media URIs from the context.
  262. *
  263. * @param {shaka.dash.DashParser.Context} context
  264. * @return {!Array.<shaka.dash.SegmentList.MediaSegment>}
  265. * @private
  266. */
  267. shaka.dash.SegmentList.parseMediaSegments_ = function(context) {
  268. var Functional = shaka.util.Functional;
  269. /** @type {!Array.<!Element>} */
  270. var segmentLists = [
  271. context.representation.segmentList,
  272. context.adaptationSet.segmentList,
  273. context.period.segmentList
  274. ].filter(Functional.isNotNull);
  275. var XmlUtils = shaka.util.XmlUtils;
  276. // Search each SegmentList for one with at least one SegmentURL element,
  277. // select the first one, and convert each SegmentURL element to a tuple.
  278. return segmentLists
  279. .map(function(node) { return XmlUtils.findChildren(node, 'SegmentURL'); })
  280. .reduce(function(all, part) { return all.length > 0 ? all : part; })
  281. .map(function(urlNode) {
  282. var uri = urlNode.getAttribute('media');
  283. var range = XmlUtils.parseAttr(
  284. urlNode, 'mediaRange', XmlUtils.parseRange, {start: 0, end: null});
  285. return {mediaUri: uri, start: range.start, end: range.end};
  286. });
  287. };