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.log');
  22. goog.require('shaka.media.SegmentIndex');
  23. goog.require('shaka.media.SegmentReference');
  24. goog.require('shaka.util.Error');
  25. goog.require('shaka.util.Functional');
  26. goog.require('shaka.util.ManifestParserUtils');
  27. goog.require('shaka.util.XmlUtils');
  28. /**
  29. * @namespace shaka.dash.SegmentList
  30. * @summary A set of functions for parsing SegmentList elements.
  31. */
  32. /**
  33. * Creates a new Stream object or updates the Stream in the manifest.
  34. *
  35. * @param {shaka.dash.DashParser.Context} context
  36. * @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
  37. * @return {shaka.dash.DashParser.StreamInfo}
  38. */
  39. shaka.dash.SegmentList.createStream = function(context, segmentIndexMap) {
  40. goog.asserts.assert(context.representation.segmentList,
  41. 'Should only be called with SegmentList');
  42. const SegmentList = shaka.dash.SegmentList;
  43. let init = shaka.dash.SegmentBase.createInitSegment(
  44. context, SegmentList.fromInheritance_);
  45. let info = SegmentList.parseSegmentListInfo_(context);
  46. SegmentList.checkSegmentListInfo_(context, info);
  47. /** @type {shaka.media.SegmentIndex} */
  48. let segmentIndex = null;
  49. let 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. let references = SegmentList.createSegmentReferences_(
  56. context.periodInfo.duration, info.startNumber,
  57. context.representation.baseUris, info);
  58. if (segmentIndex) {
  59. segmentIndex.merge(references);
  60. let start = context.presentationTimeline.getSegmentAvailabilityStart();
  61. segmentIndex.evict(start - context.periodInfo.start);
  62. } else {
  63. context.presentationTimeline.notifySegments(
  64. references, context.periodInfo.index == 0);
  65. segmentIndex = new shaka.media.SegmentIndex(references);
  66. if (id && context.dynamic) {
  67. segmentIndexMap[id] = segmentIndex;
  68. }
  69. }
  70. if (!context.dynamic || !context.periodInfo.isLastPeriod) {
  71. segmentIndex.fit(context.periodInfo.duration);
  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. scaledPresentationTimeOffset: info.scaledPresentationTimeOffset
  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. * scaledPresentationTimeOffset: 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} scaledPresentationTimeOffset
  117. * The scaledPresentationTimeOffset 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. const SegmentList = shaka.dash.SegmentList;
  141. const MpdUtils = shaka.dash.MpdUtils;
  142. let mediaSegments = SegmentList.parseMediaSegments_(context);
  143. let segmentInfo =
  144. MpdUtils.parseSegmentInfo(context, SegmentList.fromInheritance_);
  145. let startNumber = segmentInfo.startNumber;
  146. if (startNumber == 0) {
  147. shaka.log.warning('SegmentList@startNumber must be > 0');
  148. startNumber = 1;
  149. }
  150. let startTime = 0;
  151. if (segmentInfo.segmentDuration) {
  152. // See DASH sec. 5.3.9.5.3
  153. // Don't use presentationTimeOffset for @duration.
  154. startTime = segmentInfo.segmentDuration * (startNumber - 1);
  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. scaledPresentationTimeOffset: segmentInfo.scaledPresentationTimeOffset,
  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.Severity.CRITICAL,
  186. shaka.util.Error.Category.MANIFEST,
  187. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  188. }
  189. if (!info.segmentDuration && !context.periodInfo.duration && !info.timeline &&
  190. info.mediaSegments.length == 1) {
  191. shaka.log.warning(
  192. 'SegmentList does not contain sufficient segment information:',
  193. 'the SegmentList specifies one segment,',
  194. 'but does not specify a segment duration, period duration,',
  195. 'or timeline.',
  196. context.representation);
  197. throw new shaka.util.Error(
  198. shaka.util.Error.Severity.CRITICAL,
  199. shaka.util.Error.Category.MANIFEST,
  200. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  201. }
  202. if (info.timeline && info.timeline.length == 0) {
  203. shaka.log.warning(
  204. 'SegmentList does not contain sufficient segment information:',
  205. 'the SegmentList has an empty timeline.',
  206. context.representation);
  207. throw new shaka.util.Error(
  208. shaka.util.Error.Severity.CRITICAL,
  209. shaka.util.Error.Category.MANIFEST,
  210. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  211. }
  212. };
  213. /**
  214. * Creates an array of segment references for the given data.
  215. *
  216. * @param {?number} periodDuration in seconds.
  217. * @param {number} startNumber
  218. * @param {!Array.<string>} baseUris
  219. * @param {shaka.dash.SegmentList.SegmentListInfo} info
  220. * @return {!Array.<!shaka.media.SegmentReference>}
  221. * @private
  222. */
  223. shaka.dash.SegmentList.createSegmentReferences_ = function(
  224. periodDuration, startNumber, baseUris, info) {
  225. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  226. let max = info.mediaSegments.length;
  227. if (info.timeline && info.timeline.length != info.mediaSegments.length) {
  228. max = Math.min(info.timeline.length, info.mediaSegments.length);
  229. shaka.log.warning(
  230. 'The number of items in the segment timeline and the number of segment',
  231. 'URLs do not match, truncating', info.mediaSegments.length, 'to', max);
  232. }
  233. /** @type {!Array.<!shaka.media.SegmentReference>} */
  234. let references = [];
  235. let prevEndTime = info.startTime;
  236. for (let i = 0; i < max; i++) {
  237. let segment = info.mediaSegments[i];
  238. let mediaUri = ManifestParserUtils.resolveUris(
  239. baseUris, [segment.mediaUri]);
  240. let startTime = prevEndTime;
  241. let endTime;
  242. if (info.segmentDuration != null) {
  243. endTime = startTime + info.segmentDuration;
  244. } else if (info.timeline) {
  245. // Ignore the timepoint start since they are continuous.
  246. endTime = info.timeline[i].end;
  247. } else {
  248. // If segmentDuration and timeline are null then there must
  249. // be exactly one segment.
  250. goog.asserts.assert(
  251. info.mediaSegments.length == 1 && periodDuration,
  252. 'There should be exactly one segment with a Period duration.');
  253. endTime = startTime + periodDuration;
  254. }
  255. let getUris = (function(uris) { return uris; }.bind(null, mediaUri));
  256. references.push(
  257. new shaka.media.SegmentReference(
  258. i + startNumber, startTime, endTime, getUris, segment.start,
  259. segment.end));
  260. prevEndTime = endTime;
  261. }
  262. return references;
  263. };
  264. /**
  265. * Parses the media URIs from the context.
  266. *
  267. * @param {shaka.dash.DashParser.Context} context
  268. * @return {!Array.<shaka.dash.SegmentList.MediaSegment>}
  269. * @private
  270. */
  271. shaka.dash.SegmentList.parseMediaSegments_ = function(context) {
  272. const Functional = shaka.util.Functional;
  273. /** @type {!Array.<!Element>} */
  274. let segmentLists = [
  275. context.representation.segmentList,
  276. context.adaptationSet.segmentList,
  277. context.period.segmentList
  278. ].filter(Functional.isNotNull);
  279. const XmlUtils = shaka.util.XmlUtils;
  280. // Search each SegmentList for one with at least one SegmentURL element,
  281. // select the first one, and convert each SegmentURL element to a tuple.
  282. return segmentLists
  283. .map(function(node) { return XmlUtils.findChildren(node, 'SegmentURL'); })
  284. .reduce(function(all, part) { return all.length > 0 ? all : part; })
  285. .map(function(urlNode) {
  286. if (urlNode.getAttribute('indexRange') &&
  287. !context.indexRangeWarningGiven) {
  288. context.indexRangeWarningGiven = true;
  289. shaka.log.warning(
  290. 'We do not support the SegmentURL@indexRange attribute on ' +
  291. 'SegmentList. We only use the SegmentList@duration attribute ' +
  292. 'or SegmentTimeline, which must be accurate.');
  293. }
  294. let uri = urlNode.getAttribute('media');
  295. let range = XmlUtils.parseAttr(
  296. urlNode, 'mediaRange', XmlUtils.parseRange, {start: 0, end: null});
  297. return {mediaUri: uri, start: range.start, end: range.end};
  298. });
  299. };