Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create a new simple API for Audio #8005

Merged
merged 7 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/tutorials/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@ application:
- Player API Changes:
- The constructor no longer takes `mediaElement` as a parameter; use the `attach` method to attach to a media element instead. (Deprecated in v4.6)
- The `TimelineRegionInfo.eventElement` has been replaced with `TimelineRegionInfo.eventNode` property, the new property type is `shaka.externs.xml.Node` instead of `Element`
- New API for audio: `getAudioTracks` and `selectAudioTrack`, we also deprecated in v4.14 `getAudioLanguages`, `getAudioLanguagesAndRoles` and `selectAudioLanguage`.
67 changes: 67 additions & 0 deletions externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ shaka.extern.BufferedInfo;
* forced: boolean,
* videoId: ?number,
* audioId: ?number,
* audioGroupId: ?string,
* channelsCount: ?number,
* audioSamplingRate: ?number,
* tilesLayout: ?string,
Expand Down Expand Up @@ -361,6 +362,10 @@ shaka.extern.BufferedInfo;
* (only for variant tracks) The video stream id.
* @property {?number} audioId
* (only for variant tracks) The audio stream id.
* @property {?string} audioGroupId
* (only for variant tracks)
* The ID of the stream's parent element. In DASH, this will be a unique
* ID that represents the representation's parent adaptation element
* @property {?number} channelsCount
* The count of the audio track channels.
* @property {?number} audioSamplingRate
Expand Down Expand Up @@ -396,6 +401,68 @@ shaka.extern.BufferedInfo;
*/
shaka.extern.Track;

/**
* @typedef {{
* active: boolean,
* language: string,
* label: ?string,
* mimeType: ?string,
* codecs: ?string,
* primary: boolean,
* roles: !Array<string>,
* accessibilityPurpose: ?shaka.media.ManifestParser.AccessibilityPurpose,
* channelsCount: ?number,
* audioSamplingRate: ?number,
* spatialAudio: boolean,
* originalLanguage: ?string
* }}
*
* @description
* An object describing a audio track. This object should be treated as
* read-only as changing any values does not have any effect.
*
* @property {boolean} active
* If true, this is the track being streamed (another track may be
* visible/audible in the buffer).
*
* @property {string} language
* The language of the track, or <code>'und'</code> if not given. This value
* is normalized as follows - language part is always lowercase and translated
* to ISO-639-1 when possible, locale part is always uppercase,
* i.e. <code>'en-US'</code>.
* @property {?string} label
* The track label, which is unique text that should describe the track.
* @property {?string} mimeType
* The MIME type of the content provided in the manifest.
* @property {?string} codecs
* The audio codecs string provided in the manifest, if present.
* @property {boolean} primary
* True indicates that this in the primary language for the content.
* This flag is based on signals from the manifest.
* This can be a useful hint about which language should be the default, and
* indicates which track Shaka will use when the user's language preference
* cannot be satisfied.
* @property {!Array<string>} roles
* The roles of the track, e.g. <code>'main'</code>, <code>'caption'</code>,
* or <code>'commentary'</code>.
* @property {?shaka.media.ManifestParser.AccessibilityPurpose
* } accessibilityPurpose
* The DASH accessibility descriptor, if one was provided for this track.
* @property {?number} channelsCount
* The count of the audio track channels.
* @property {?number} audioSamplingRate
* Specifies the maximum sampling rate of the content.
* @property {boolean} spatialAudio
* True indicates that the content has spatial audio.
* This flag is based on signals from the manifest.
* @property {?string} originalLanguage
* The original language of the track, if any, as it appeared in the original
* manifest. This is the exact value provided in the manifest; for normalized
* value use <code>language</code> property.
* @exportDoc
*/
shaka.extern.AudioTrack;


/**
* @typedef {!Array<!shaka.extern.Track>}
Expand Down
2 changes: 2 additions & 0 deletions lib/cast/cast_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ shaka.cast.CastUtils.LargePlayerGetterMethods = new Map()
.set('getConfiguration', 4)
.set('getConfigurationForLowLatency', 4)
.set('getStats', 5)
.set('getAudioTracks', 2)
.set('getTextTracks', 2)
.set('getVariantTracks', 2);

Expand Down Expand Up @@ -402,6 +403,7 @@ shaka.cast.CastUtils.PlayerVoidMethods = [
'resetConfiguration',
'retryStreaming',
'selectAudioLanguage',
'selectAudioTrack',
'selectTextLanguage',
'selectTextTrack',
'selectVariantTrack',
Expand Down
159 changes: 159 additions & 0 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ goog.require('shaka.text.TextEngine');
goog.require('shaka.text.Utils');
goog.require('shaka.text.UITextDisplayer');
goog.require('shaka.text.WebVttGenerator');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.CmcdManager');
goog.require('shaka.util.CmsdManager');
Expand Down Expand Up @@ -273,6 +274,17 @@ goog.requireType('shaka.media.PresentationTimeline');
*/


/**
* @event shaka.Player.AudioTracksChangedEvent
* @description Fired when the list of audio tracks changes.
* An app may want to look at <code>getAudioTracks()</code> to see what
* happened.
* @property {string} type
* 'audiotrackschanged'
* @exportDoc
*/


/**
* @event shaka.Player.TracksChangedEvent
* @description Fired when the list of tracks changes. For example, this will
Expand Down Expand Up @@ -5276,11 +5288,111 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
selectSrcEqualsMode();
}

/**
* Select an audio track compatible with the current video track.
* If the player has not loaded any content, this will be a no-op.
*
* @param {shaka.extern.AudioTrack} audioTrack
* @param {number=} safeMargin Optional amount of buffer (in seconds) to
* retain when clearing the buffer. Useful for switching quickly
* without causing a buffering event. Defaults to 0 if not provided. Can
* cause hiccups on some browsers if chosen too small, e.g. The amount of
* two segments is a fair minimum to consider as safeMargin value.
* @export
*/
selectAudioTrack(audioTrack, safeMargin = 0) {
const ArrayUtils = shaka.util.ArrayUtils;
const variants = this.getVariantTracks();
avelad marked this conversation as resolved.
Show resolved Hide resolved
if (!variants.length) {
return;
}
const active = variants.find((t) => t.active);
if (!active) {
return;
}
const validVariant = variants.find((t) => {
return t.videoId === active.videoId &&
t.language == audioTrack.language &&
t.label == audioTrack.label &&
t.audioMimeType == audioTrack.mimeType &&
t.audioCodec == audioTrack.codecs &&
t.primary == audioTrack.primary &&
ArrayUtils.equal(t.audioRoles, audioTrack.roles) &&
t.accessibilityPurpose == audioTrack.accessibilityPurpose &&
t.channelsCount == audioTrack.channelsCount &&
t.audioSamplingRate == audioTrack.audioSamplingRate &&
t.spatialAudio == audioTrack.spatialAudio;
});
if (validVariant && !validVariant.active) {
this.selectVariantTrack(validVariant,
/* clearBuffer= */ true, safeMargin);
}
}


/**
* Return a list of audio tracks compatible with the current video track.
*
* @return {!Array<shaka.extern.AudioTrack>}
* @export
*/
getAudioTracks() {
const variants = this.getVariantTracks();
avelad marked this conversation as resolved.
Show resolved Hide resolved
if (!variants.length) {
return [];
}
const active = variants.find((t) => t.active);
if (!active) {
return [];
}
let filteredTracks = variants;
if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
// Filter by current videoId and has audio.
filteredTracks = variants.filter((t) => {
return t.originalVideoId === active.originalVideoId && t.audioCodec;
});
}
if (!filteredTracks.length) {
return [];
}

/** @type {!Set<shaka.extern.AudioTrack>} */
const audioTracksSet = new Set();
for (const track of filteredTracks) {
/** @type {shaka.extern.AudioTrack} */
const audioTrack = {
active: track.active,
language: track.language,
label: track.label,
mimeType: track.audioMimeType,
codecs: track.audioCodec,
primary: track.primary,
roles: track.audioRoles || [],
accessibilityPurpose: track.accessibilityPurpose,
channelsCount: track.channelsCount,
audioSamplingRate: track.audioSamplingRate,
spatialAudio: track.spatialAudio,
originalLanguage: track.originalLanguage,
};
audioTracksSet.add(audioTrack);
}
if (!audioTracksSet.size) {
return [];
}
return Array.from(audioTracksSet);
}

/**
* Return a list of audio language-role combinations available. If the
* player has not loaded any content, this will return an empty list.
*
* <br>
*
* This API is deprecated and will be removed in version 5.0, please migrate
* to using `getAudioTracks` and `selectAudioTrack`.
*
* @return {!Array<shaka.extern.LanguageRole>}
* @deprecated
* @export
*/
getAudioLanguagesAndRoles() {
Expand All @@ -5302,7 +5414,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
* Return a list of audio languages available. If the player has not loaded
* any content, this will return an empty list.
*
* <br>
*
* This API is deprecated and will be removed in version 5.0, please migrate
* to using `getAudioTracks` and `selectAudioTrack`.
*
* @return {!Array<string>}
* @deprecated
* @export
*/
getAudioLanguages() {
Expand All @@ -5325,13 +5443,19 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
* language, role and channel count, and chooses a new variant if need be.
* If the player has not loaded any content, this will be a no-op.
*
* <br>
*
* This API is deprecated and will be removed in version 5.0, please migrate
* to using `getAudioTracks` and `selectAudioTrack`.
*
* @param {string} language
* @param {string=} role
* @param {number=} channelsCount
* @param {number=} safeMargin
* @param {string=} codec
* @param {boolean=} spatialAudio
* @param {string=} label
* @deprecated
* @export
*/
selectAudioLanguage(language, role, channelsCount = 0, safeMargin = 0,
Expand Down Expand Up @@ -7428,6 +7552,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
// Dispatch a 'variantchanged' event
this.onVariantChanged_(oldTrack, newTrack);
}
// Dispatch a 'audiotrackschanged' event if necessary
this.checkAudioTracksChanged_(oldTrack, newTrack);
}

/**
Expand Down Expand Up @@ -7457,7 +7583,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
shaka.util.StreamUtils.html5AudioTrackToTrack(currentTrack);
const newTrack =
shaka.util.StreamUtils.html5AudioTrackToTrack(track);
// Dispatch a 'variantchanged' event
this.onVariantChanged_(oldTrack, newTrack);

// Dispatch a 'audiotrackschanged' event if necessary
this.checkAudioTracksChanged_(oldTrack, newTrack);
}

/**
Expand Down Expand Up @@ -7668,6 +7798,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
const event = shaka.Player.makeEvent_(
shaka.util.FakeEvent.EventName.TracksChanged);
this.delayDispatchEvent_(event);

// Also fire 'audiotrackschanged' event.
this.onAudioTracksChanged_();
}

/**
Expand All @@ -7691,6 +7824,32 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.delayDispatchEvent_(event);
}

/**
* Dispatches a 'audiotrackschanged' event if necessary
* @param {?shaka.extern.Track} from
* @param {shaka.extern.Track} to
* @private
*/
checkAudioTracksChanged_(from, to) {
let dispatchEvent = false;
if (!from || from.audioId != to.audioId ||
from.audioGroupId != to.audioGroupId) {
dispatchEvent = true;
}
if (dispatchEvent) {
this.onAudioTracksChanged_();
}
}

/** @private */
onAudioTracksChanged_() {
// Delay the 'audiotrackschanged' event so StreamingEngine has time to
// absorb the changes before the user tries to query it.
const event = shaka.Player.makeEvent_(
shaka.util.FakeEvent.EventName.AudioTracksChanged);
this.delayDispatchEvent_(event);
}

/**
* Dispatches a 'textchanged' event.
* @private
Expand Down
10 changes: 8 additions & 2 deletions lib/util/array_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,19 @@ shaka.util.ArrayUtils = class {
/**
* Determines if the given arrays contain equal elements in the same order.
*
* @param {!Array<T>} a
* @param {!Array<T>} b
* @param {Array<T>} a
* @param {Array<T>} b
* @param {function(T, T):boolean=} compareFn
* @return {boolean}
* @template T
*/
static equal(a, b, compareFn) {
if (a === b) {
return true;
}
if (!a || !b) {
return a == b;
}
if (!compareFn) {
compareFn = shaka.util.ArrayUtils.defaultEquals;
}
Expand Down
1 change: 1 addition & 0 deletions lib/util/fake_event.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ shaka.util.FakeEvent.EventName = {
AbrStatusChanged: 'abrstatuschanged',
Adaptation: 'adaptation',
AudioTrackChanged: 'audiotrackchanged',
AudioTracksChanged: 'audiotrackschanged',
Buffering: 'buffering',
Complete: 'complete',
DownloadCompleted: 'downloadcompleted',
Expand Down
Loading