mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
7c1e31d4e6
Now that jsdoc supports this, it will make future diffs cleaner. See #8819 and #1236.
341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.media.QualityObserver');
|
|
|
|
goog.require('shaka.media.IPlayheadObserver');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.util.ArrayUtils');
|
|
goog.require('shaka.util.FakeEvent');
|
|
goog.require('shaka.util.FakeEventTarget');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
|
|
/**
|
|
* Monitors the quality of content being appended to the source buffers and
|
|
* fires 'qualitychange' events when the media quality at the playhead changes.
|
|
*
|
|
* @implements {shaka.media.IPlayheadObserver}
|
|
* @final
|
|
*/
|
|
shaka.media.QualityObserver = class extends shaka.util.FakeEventTarget {
|
|
/**
|
|
* Creates a new QualityObserver.
|
|
*
|
|
* @param {!function():!shaka.extern.BufferedInfo} getBufferedInfo
|
|
* Buffered info is needed to purge QualityChanges that are no
|
|
* longer relevant.
|
|
*/
|
|
constructor(getBufferedInfo) {
|
|
super();
|
|
|
|
/**
|
|
* @private {!Map<string, !shaka.media.QualityObserver.ContentTypeState>}
|
|
*/
|
|
this.contentTypeStates_ = new Map();
|
|
|
|
/** @private function():!shaka.extern.BufferedInfo */
|
|
this.getBufferedInfo_ = getBufferedInfo;
|
|
}
|
|
|
|
/** @override */
|
|
release() {
|
|
this.contentTypeStates_.clear();
|
|
super.release();
|
|
}
|
|
|
|
/**
|
|
* Get the ContentTypeState for a contentType, creating a new
|
|
* one if necessary.
|
|
*
|
|
* @param {!string} contentType
|
|
* The contend type e.g. "video" or "audio".
|
|
* @return {!shaka.media.QualityObserver.ContentTypeState}
|
|
* @private
|
|
*/
|
|
getContentTypeState_(contentType) {
|
|
let contentTypeState = this.contentTypeStates_.get(contentType);
|
|
if (!contentTypeState) {
|
|
contentTypeState = {
|
|
qualityChangePositions: [],
|
|
currentQuality: null,
|
|
contentType: contentType,
|
|
};
|
|
this.contentTypeStates_.set(contentType, contentTypeState);
|
|
}
|
|
return contentTypeState;
|
|
}
|
|
|
|
/**
|
|
* Adds a QualityChangePosition for the contentType identified by
|
|
* the mediaQuality.contentType.
|
|
*
|
|
* @param {!shaka.extern.MediaQualityInfo} mediaQuality
|
|
* @param {!number} position
|
|
* Position in seconds of the quality change.
|
|
*/
|
|
addMediaQualityChange(mediaQuality, position) {
|
|
const contentTypeState =
|
|
this.getContentTypeState_(mediaQuality.contentType);
|
|
|
|
// Remove unneeded QualityChangePosition(s) before adding the new one
|
|
this.purgeQualityChangePositions_(contentTypeState);
|
|
|
|
const newChangePosition = {
|
|
mediaQuality: mediaQuality,
|
|
position: position,
|
|
};
|
|
|
|
const changePositions = contentTypeState.qualityChangePositions;
|
|
const insertBeforeIndex = changePositions.findIndex(
|
|
(qualityChange) => (qualityChange.position >= position));
|
|
|
|
if (insertBeforeIndex >= 0) {
|
|
const duplicatePositions =
|
|
(changePositions[insertBeforeIndex].position == position) ? 1 : 0;
|
|
changePositions.splice(
|
|
insertBeforeIndex, duplicatePositions, newChangePosition);
|
|
} else {
|
|
changePositions.push(newChangePosition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines the media quality at a specific position in the source buffer.
|
|
*
|
|
* @param {!number} position
|
|
* Position in seconds
|
|
* @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
|
|
* @return {?shaka.extern.MediaQualityInfo}
|
|
* @private
|
|
*/
|
|
static getMediaQualityAtPosition_(position, contentTypeState) {
|
|
// The qualityChangePositions must be ordered by position ascending
|
|
// Find the last QualityChangePosition prior to the position
|
|
const changePositions = contentTypeState.qualityChangePositions;
|
|
for (let i = changePositions.length - 1; i >= 0; i--) {
|
|
const qualityChange = changePositions[i];
|
|
if (qualityChange.position <= position) {
|
|
return qualityChange.mediaQuality;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Determines if two MediaQualityInfo objects are the same or not.
|
|
*
|
|
* @param {?shaka.extern.MediaQualityInfo} mq1
|
|
* @param {?shaka.extern.MediaQualityInfo} mq2
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
static mediaQualitiesAreTheSame_(mq1, mq2) {
|
|
if (mq1 === mq2) {
|
|
return true;
|
|
}
|
|
if (!mq1 || !mq2) {
|
|
return false;
|
|
}
|
|
return (mq1.bandwidth == mq2.bandwidth) &&
|
|
(mq1.audioSamplingRate == mq2.audioSamplingRate) &&
|
|
(mq1.codecs == mq2.codecs) &&
|
|
(mq1.contentType == mq2.contentType) &&
|
|
(mq1.frameRate == mq2.frameRate) &&
|
|
(mq1.height == mq2.height) &&
|
|
(mq1.mimeType == mq2.mimeType) &&
|
|
(mq1.channelsCount == mq2.channelsCount) &&
|
|
(mq1.pixelAspectRatio == mq2.pixelAspectRatio) &&
|
|
(mq1.width == mq2.width);
|
|
}
|
|
|
|
/** @override */
|
|
poll(positionInSeconds, wasSeeking) {
|
|
for (const contentTypeState of this.contentTypeStates_.values()) {
|
|
const currentQuality = contentTypeState.currentQuality;
|
|
const qualityAtPosition =
|
|
shaka.media.QualityObserver.getMediaQualityAtPosition_(
|
|
positionInSeconds, contentTypeState);
|
|
|
|
const differentQualities = qualityAtPosition &&
|
|
!shaka.media.QualityObserver.mediaQualitiesAreTheSame_(
|
|
currentQuality, qualityAtPosition);
|
|
const differentLabel = qualityAtPosition && currentQuality &&
|
|
qualityAtPosition.label && currentQuality.label &&
|
|
currentQuality.label !== qualityAtPosition.label;
|
|
const differentLanguage = qualityAtPosition && currentQuality &&
|
|
qualityAtPosition.language && currentQuality.language &&
|
|
currentQuality.language !== qualityAtPosition.language;
|
|
const differentRoles = qualityAtPosition && currentQuality &&
|
|
qualityAtPosition.roles && currentQuality.roles &&
|
|
!shaka.util.ArrayUtils.equal(currentQuality.roles,
|
|
qualityAtPosition.roles);
|
|
|
|
if (differentLabel || differentLanguage || differentRoles) {
|
|
if (this.positionIsBuffered_(
|
|
positionInSeconds, qualityAtPosition.contentType)) {
|
|
contentTypeState.currentQuality = qualityAtPosition;
|
|
|
|
const event = new shaka.util.FakeEvent('audiotrackchange', new Map([
|
|
['quality', qualityAtPosition],
|
|
['position', positionInSeconds],
|
|
]));
|
|
this.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
if (differentQualities) {
|
|
if (this.positionIsBuffered_(
|
|
positionInSeconds, qualityAtPosition.contentType)) {
|
|
contentTypeState.currentQuality = qualityAtPosition;
|
|
|
|
shaka.log.debug('Media quality changed at position ' +
|
|
positionInSeconds + ' ' + JSON.stringify(qualityAtPosition));
|
|
|
|
const event = new shaka.util.FakeEvent('qualitychange', new Map([
|
|
['quality', qualityAtPosition],
|
|
['position', positionInSeconds],
|
|
]));
|
|
this.dispatchEvent(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if a position is buffered for a given content type.
|
|
*
|
|
* @param {!number} position
|
|
* @param {!string} contentType
|
|
* @private
|
|
*/
|
|
positionIsBuffered_(position, contentType) {
|
|
const bufferedInfo = this.getBufferedInfo_();
|
|
const bufferedRanges = bufferedInfo[contentType];
|
|
if (bufferedRanges && bufferedRanges.length > 0) {
|
|
const bufferStart = bufferedRanges[0].start;
|
|
const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
|
|
if (position >= bufferStart && position < bufferEnd) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes the QualityChangePosition(s) that are not relevant to the buffered
|
|
* content of the specified contentType. Note that this function is
|
|
* invoked just before adding the quality change info associated with
|
|
* the next media segment to be appended.
|
|
*
|
|
* @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
|
|
* @private
|
|
*/
|
|
purgeQualityChangePositions_(contentTypeState) {
|
|
const bufferedInfo = this.getBufferedInfo_();
|
|
const bufferedRanges = bufferedInfo[contentTypeState.contentType];
|
|
|
|
if (bufferedRanges && bufferedRanges.length > 0) {
|
|
const bufferStart = bufferedRanges[0].start;
|
|
const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
|
|
const oldChangePositions = contentTypeState.qualityChangePositions;
|
|
contentTypeState.qualityChangePositions =
|
|
oldChangePositions.filter(
|
|
(qualityChange, index) => {
|
|
// Remove all but last quality change before bufferStart.
|
|
if ((qualityChange.position <= bufferStart) &&
|
|
(index + 1 < oldChangePositions.length) &&
|
|
(oldChangePositions[index + 1].position <= bufferStart)) {
|
|
return false;
|
|
}
|
|
// Remove all quality changes after bufferEnd.
|
|
if (qualityChange.position >= bufferEnd) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
} else {
|
|
// Nothing is buffered; so remove all quality changes.
|
|
contentTypeState.qualityChangePositions = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a MediaQualityInfo object from a stream object.
|
|
*
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @return {!shaka.extern.MediaQualityInfo}
|
|
*/
|
|
static createQualityInfo(stream) {
|
|
const basicQuality = {
|
|
bandwidth: stream.bandwidth || 0,
|
|
audioSamplingRate: null,
|
|
codecs: stream.codecs,
|
|
contentType: stream.type,
|
|
frameRate: null,
|
|
height: null,
|
|
mimeType: stream.mimeType,
|
|
channelsCount: null,
|
|
pixelAspectRatio: null,
|
|
width: null,
|
|
label: null,
|
|
roles: stream.roles,
|
|
language: null,
|
|
};
|
|
if (stream.type == shaka.util.ManifestParserUtils.ContentType.VIDEO) {
|
|
basicQuality.frameRate = stream.frameRate || null;
|
|
basicQuality.height = stream.height || null;
|
|
basicQuality.pixelAspectRatio = stream.pixelAspectRatio || null;
|
|
basicQuality.width = stream.width || null;
|
|
}
|
|
if (stream.type == shaka.util.ManifestParserUtils.ContentType.AUDIO) {
|
|
basicQuality.audioSamplingRate = stream.audioSamplingRate;
|
|
basicQuality.channelsCount = stream.channelsCount;
|
|
basicQuality.label = stream.label || null;
|
|
basicQuality.language = stream.language;
|
|
}
|
|
return basicQuality;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @typedef {{
|
|
* mediaQuality: !shaka.extern.MediaQualityInfo,
|
|
* position: !number,
|
|
* }}
|
|
*
|
|
* @description
|
|
* Identifies the position of a media quality change in the
|
|
* source buffer.
|
|
*
|
|
* @property {!shaka.extern.MediaQualityInfo} mediaQuality
|
|
* The new media quality for content after position in the source buffer.
|
|
* @property {!number} position
|
|
* A position in seconds in the source buffer
|
|
*/
|
|
shaka.media.QualityObserver.QualityChangePosition;
|
|
|
|
/**
|
|
* @typedef {{
|
|
* qualityChangePositions:
|
|
* !Array<shaka.media.QualityObserver.QualityChangePosition>,
|
|
* currentQuality: ?shaka.extern.MediaQualityInfo,
|
|
* contentType: !string,
|
|
* }}
|
|
*
|
|
* @description
|
|
* Contains media quality information for a specific content type
|
|
* e.g. video or audio.
|
|
*
|
|
* @property {!Array<shaka.media.QualityObserver.QualityChangePosition>
|
|
* } qualityChangePositions
|
|
* Quality changes ordered by position ascending.
|
|
* @property {?shaka.media.MediaQualityInfo} currentMediaQuality
|
|
* The media quality at the playhead position.
|
|
* @property {string} contentType
|
|
* The contentType e.g. 'video' or 'audio'
|
|
*/
|
|
shaka.media.QualityObserver.ContentTypeState;
|