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