Source: lib/media/quality_observer.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.QualityObserver');
  7. goog.require('shaka.media.IPlayheadObserver');
  8. goog.require('shaka.log');
  9. goog.require('shaka.util.FakeEvent');
  10. goog.require('shaka.util.FakeEventTarget');
  11. /**
  12. * Monitors the quality of content being appended to the source buffers and
  13. * fires 'qualitychange' events when the media quality at the playhead changes.
  14. *
  15. * @implements {shaka.media.IPlayheadObserver}
  16. * @final
  17. */
  18. shaka.media.QualityObserver = class extends shaka.util.FakeEventTarget {
  19. /**
  20. * Creates a new QualityObserver.
  21. *
  22. * @param {!function():!shaka.extern.BufferedInfo} getBufferedInfo
  23. * Buffered info is needed to purge QualityChanges that are no
  24. * longer relevant.
  25. */
  26. constructor(getBufferedInfo) {
  27. super();
  28. /**
  29. * @private {!Map.<string,!shaka.media.QualityObserver.ContentTypeState>}
  30. */
  31. this.contentTypeStates_ = new Map();
  32. /** @private function():!shaka.extern.BufferedInfo */
  33. this.getBufferedInfo_ = getBufferedInfo;
  34. }
  35. /** @override */
  36. release() {
  37. this.contentTypeStates_.clear();
  38. super.release();
  39. }
  40. /**
  41. * Get the ContenTypeState for a contentType, creating a new
  42. * one if necessary.
  43. *
  44. * @param {!string} contentType
  45. * The contend type e.g. "video" or "audio".
  46. * @return {!shaka.media.QualityObserver.ContentTypeState}
  47. * @private
  48. */
  49. getContentTypeState_(contentType) {
  50. let contentTypeState = this.contentTypeStates_.get(contentType);
  51. if (!contentTypeState) {
  52. contentTypeState = {
  53. qualityChangePositions: [],
  54. currentQuality: null,
  55. contentType: contentType,
  56. };
  57. this.contentTypeStates_.set(contentType, contentTypeState);
  58. }
  59. return contentTypeState;
  60. }
  61. /**
  62. * Adds a QualityChangePosition for the contentType identified by
  63. * the mediaQuality.contentType.
  64. *
  65. * @param {!shaka.extern.MediaQualityInfo} mediaQuality
  66. * @param {!number} position
  67. * Position in seconds of the quality change.
  68. */
  69. addMediaQualityChange(mediaQuality, position) {
  70. const contentTypeState =
  71. this.getContentTypeState_(mediaQuality.contentType);
  72. // Remove unneeded QualityChangePosition(s) before adding the new one
  73. this.purgeQualityChangePositions_(contentTypeState);
  74. const newChangePosition = {
  75. mediaQuality: mediaQuality,
  76. position: position,
  77. };
  78. const changePositions = contentTypeState.qualityChangePositions;
  79. const insertBeforeIndex = changePositions.findIndex(
  80. (qualityChange) => (qualityChange.position >= position));
  81. if (insertBeforeIndex >= 0) {
  82. const duplicatePositions =
  83. (changePositions[insertBeforeIndex].position == position) ? 1 : 0;
  84. changePositions.splice(
  85. insertBeforeIndex, duplicatePositions, newChangePosition);
  86. } else {
  87. changePositions.push(newChangePosition);
  88. }
  89. }
  90. /**
  91. * Determines the media quality at a specific position in the source buffer.
  92. *
  93. * @param {!number} position
  94. * Position in seconds
  95. * @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
  96. * @return {?shaka.extern.MediaQualityInfo}
  97. * @private
  98. */
  99. static getMediaQualityAtPosition_(position, contentTypeState) {
  100. // The qualityChangePositions must be ordered by position ascending
  101. // Find the last QualityChangePosition prior to the position
  102. const changePositions = contentTypeState.qualityChangePositions;
  103. for (let i = changePositions.length - 1; i >= 0; i--) {
  104. const qualityChange = changePositions[i];
  105. if (qualityChange.position <= position) {
  106. return qualityChange.mediaQuality;
  107. }
  108. }
  109. return null;
  110. }
  111. /**
  112. * Determines if two MediaQualityInfo objects are the same or not.
  113. *
  114. * @param {?shaka.extern.MediaQualityInfo} mq1
  115. * @param {?shaka.extern.MediaQualityInfo} mq2
  116. * @return {boolean}
  117. * @private
  118. */
  119. static mediaQualitiesAreTheSame_(mq1, mq2) {
  120. if (mq1 === mq2) {
  121. return true;
  122. }
  123. if (!mq1 || !mq2) {
  124. return false;
  125. }
  126. return (mq1.bandwidth == mq2.bandwidth) &&
  127. (mq1.audioSamplingRate == mq2.audioSamplingRate) &&
  128. (mq1.codecs == mq2.codecs) &&
  129. (mq1.contentType == mq2.contentType) &&
  130. (mq1.frameRate == mq2.frameRate) &&
  131. (mq1.height == mq2.height) &&
  132. (mq1.mimeType == mq2.mimeType) &&
  133. (mq1.channelsCount == mq2.channelsCount) &&
  134. (mq1.pixelAspectRatio == mq2.pixelAspectRatio) &&
  135. (mq1.width == mq2.width);
  136. }
  137. /** @override */
  138. poll(positionInSeconds, wasSeeking) {
  139. for (const contentTypeState of this.contentTypeStates_.values()) {
  140. const qualityAtPosition =
  141. shaka.media.QualityObserver.getMediaQualityAtPosition_(
  142. positionInSeconds, contentTypeState);
  143. if (qualityAtPosition &&
  144. !shaka.media.QualityObserver.mediaQualitiesAreTheSame_(
  145. contentTypeState.currentQuality, qualityAtPosition)) {
  146. if (this.positionIsBuffered_(
  147. positionInSeconds, qualityAtPosition.contentType)) {
  148. contentTypeState.currentQuality = qualityAtPosition;
  149. shaka.log.debug('Media quality changed at position ' +
  150. positionInSeconds + ' ' + JSON.stringify(qualityAtPosition));
  151. const event = new shaka.util.FakeEvent('qualitychange', new Map([
  152. ['quality', qualityAtPosition],
  153. ['position', positionInSeconds],
  154. ]));
  155. this.dispatchEvent(event);
  156. }
  157. }
  158. }
  159. }
  160. /**
  161. * Determine if a position is buffered for a given content type.
  162. *
  163. * @param {!number} position
  164. * @param {!string} contentType
  165. * @private
  166. */
  167. positionIsBuffered_(position, contentType) {
  168. const bufferedInfo = this.getBufferedInfo_();
  169. const bufferedRanges = bufferedInfo[contentType];
  170. if (bufferedRanges && bufferedRanges.length > 0) {
  171. const bufferStart = bufferedRanges[0].start;
  172. const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
  173. if (position >= bufferStart && position < bufferEnd) {
  174. return true;
  175. }
  176. }
  177. return false;
  178. }
  179. /**
  180. * Removes the QualityChangePosition(s) that are not relevant to the buffered
  181. * content of the specified contentType. Note that this function is
  182. * invoked just before adding the quality change info associated with
  183. * the next media segment to be appended.
  184. *
  185. * @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
  186. * @private
  187. */
  188. purgeQualityChangePositions_(contentTypeState) {
  189. const bufferedInfo = this.getBufferedInfo_();
  190. const bufferedRanges = bufferedInfo[contentTypeState.contentType];
  191. if (bufferedRanges && bufferedRanges.length > 0) {
  192. const bufferStart = bufferedRanges[0].start;
  193. const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
  194. const oldChangePositions = contentTypeState.qualityChangePositions;
  195. contentTypeState.qualityChangePositions =
  196. oldChangePositions.filter(
  197. (qualityChange, index) => {
  198. // Remove all but last quality change before bufferStart.
  199. if ((qualityChange.position <= bufferStart) &&
  200. (index + 1 < oldChangePositions.length) &&
  201. (oldChangePositions[index + 1].position <= bufferStart)) {
  202. return false;
  203. }
  204. // Remove all quality changes after bufferEnd.
  205. if (qualityChange.position >= bufferEnd) {
  206. return false;
  207. }
  208. return true;
  209. });
  210. } else {
  211. // Nothing is buffered; so remove all quality changes.
  212. contentTypeState.qualityChangePositions = [];
  213. }
  214. }
  215. };
  216. /**
  217. * @typedef {{
  218. * mediaQuality: !shaka.extern.MediaQualityInfo,
  219. * position: !number
  220. * }}
  221. *
  222. * @description
  223. * Identifies the position of a media quality change in the
  224. * source buffer.
  225. *
  226. * @property {shaka.extern.MediaQualityInfo} !mediaQuality
  227. * The new media quality for content after position in the source buffer.
  228. * @property {number} !position
  229. * A position in seconds in the source buffer
  230. */
  231. shaka.media.QualityObserver.QualityChangePosition;
  232. /**
  233. * @typedef {{
  234. * qualityChangePositions:
  235. * !Array.<shaka.media.QualityObserver.QualityChangePosition>,
  236. * currentQuality: ?shaka.extern.MediaQualityInfo,
  237. * contentType: !string
  238. * }}
  239. *
  240. * @description
  241. * Contains media quality information for a specific content type
  242. * e.g. video or audio.
  243. *
  244. * @property {!Array.<shaka.media.QualityObserver.QualityChangePosition>}
  245. * qualityChangePositions
  246. * Quality changes ordered by position ascending.
  247. * @property {?shaka.media.MediaQualityInfo} currentMediaQuality
  248. * The media quality at the playhead position.
  249. * @property {string} contentType
  250. * The contentType e.g. 'video' or 'audio'
  251. */
  252. shaka.media.QualityObserver.ContentTypeState;