diff --git a/src/i18n/en-US.properties b/src/i18n/en-US.properties
index 549e83a61..951cf365a 100644
--- a/src/i18n/en-US.properties
+++ b/src/i18n/en-US.properties
@@ -88,6 +88,10 @@ of=of
subtitles=Subtitles
# Label for turning off subtitles in media player
off=Off
+# Label for audio tracks in media player
+media_audio=Audio
+# Label for alternate audio tracks in media player
+track=Track
# 3D Preview
diff --git a/src/lib/viewers/media/DashViewer.js b/src/lib/viewers/media/DashViewer.js
index a1c258c21..c2d18d622 100644
--- a/src/lib/viewers/media/DashViewer.js
+++ b/src/lib/viewers/media/DashViewer.js
@@ -12,6 +12,8 @@ const CSS_CLASS_HD = 'bp-media-controls-is-hd';
const SEGMENT_SIZE = 5;
const MAX_BUFFER = SEGMENT_SIZE * 12; // 60 sec
const MANIFEST = 'manifest.mpd';
+const DEFAULT_VIDEO_WIDTH_PX = 854;
+const DEFAULT_VIDEO_HEIGHT_PX = 480;
@autobind
class DashViewer extends VideoBaseViewer {
@@ -27,9 +29,10 @@ class DashViewer extends VideoBaseViewer {
this.switchHistory = [];
// tracks
- this.hdRepresentation = {};
- this.sdRepresentation = {};
+ this.hdVideoId = -1;
+ this.sdVideoId = -1;
this.textTracks = []; // Must be sorted by representation id
+ this.audioTracks = [];
// dash specific class
this.wrapperEl.classList.add(CSS_CLASS_DASH);
@@ -191,25 +194,21 @@ class DashViewer extends VideoBaseViewer {
}
/**
- * Handler for hd video
+ * Given a videoId (e.g. hd video id), enables the track with that video ID
+ * while maintaining the SAME AUDIO as the active track.
*
* @private
+ * @param {number} videoId - The id of the video used in the variant (provided by Shaka)
* @return {void}
*/
- enableHD() {
- this.showLoadingIcon(this.hdRepresentation.id);
- this.player.selectVariantTrack(this.hdRepresentation, true);
- }
-
- /**
- * Handler for sd video
- *
- * @private
- * @return {void}
- */
- enableSD() {
- this.showLoadingIcon(this.sdRepresentation.id);
- this.player.selectVariantTrack(this.sdRepresentation, true);
+ enableVideoId(videoId) {
+ const tracks = this.player.getVariantTracks();
+ const activeTrack = this.getActiveTrack();
+ const newTrack = tracks.find((track) => track.videoId === videoId && track.audioId === activeTrack.audioId);
+ if (newTrack && newTrack.id !== activeTrack.id) {
+ this.showLoadingIcon(newTrack.id);
+ this.player.selectVariantTrack(newTrack, true);
+ }
}
/**
@@ -244,6 +243,21 @@ class DashViewer extends VideoBaseViewer {
}
}
+ /**
+ * Handler for audio track
+ *
+ * @private
+ * @emits audiochange
+ * @return {void}
+ */
+ handleAudioTrack() {
+ const audioIdx = parseInt(this.cache.get('media-audiotracks'), 10);
+ if (this.audioTracks[audioIdx] !== undefined) {
+ const track = this.audioTracks[audioIdx];
+ this.player.selectAudioLanguage(track.language, track.role);
+ }
+ }
+
/**
* Handler for hd/sd/auto video
*
@@ -257,11 +271,11 @@ class DashViewer extends VideoBaseViewer {
switch (quality) {
case 'hd':
this.enableAdaptation(false);
- this.enableHD();
+ this.enableVideoId(this.hdVideoId);
break;
case 'sd':
this.enableAdaptation(false);
- this.enableSD();
+ this.enableVideoId(this.sdVideoId);
break;
case 'auto':
default:
@@ -283,7 +297,7 @@ class DashViewer extends VideoBaseViewer {
*/
adaptationHandler() {
const activeTrack = this.getActiveTrack();
- if (activeTrack.id === this.hdRepresentation.id) {
+ if (activeTrack.videoId === this.hdVideoId) {
this.wrapperEl.classList.add(CSS_CLASS_HD);
} else {
this.wrapperEl.classList.remove(CSS_CLASS_HD);
@@ -329,6 +343,7 @@ class DashViewer extends VideoBaseViewer {
super.addEventListenersForMediaControls();
this.mediaControls.addListener('qualitychange', this.handleQuality);
this.mediaControls.addListener('subtitlechange', this.handleSubtitle);
+ this.mediaControls.addListener('audiochange', this.handleAudioTrack);
}
/**
@@ -345,6 +360,38 @@ class DashViewer extends VideoBaseViewer {
);
}
}
+
+ /**
+ * Loads alternate audio streams
+ *
+ * @return {void}
+ */
+ loadAlternateAudio() {
+ const variants = this.player.getVariantTracks().sort((track1, track2) => track1.audioId - track2.audioId);
+ const audioIds = [];
+ const uniqueAudioVariants = [];
+
+ let i = 0;
+ for (i = 0; i < variants.length; i++) {
+ const audioTrack = variants[i];
+ if (audioIds.indexOf(audioTrack.audioId) < 0) {
+ audioIds.push(audioTrack.audioId);
+ uniqueAudioVariants.push(audioTrack);
+ }
+ }
+
+ this.audioTracks = uniqueAudioVariants.map((track) => ({
+ language: track.language,
+ role: track.roles[0]
+ }));
+
+ if (this.audioTracks.length > 1) {
+ // translate the language first
+ const languages = this.audioTracks.map((track) => getLanguageName(track.language) || track.language);
+ this.mediaControls.initAlternateAudio(languages);
+ }
+ }
+
/**
* Handler for meta data load for the media element.
*
@@ -365,6 +412,7 @@ class DashViewer extends VideoBaseViewer {
this.startBandwidthTracking();
this.handleQuality(); // should come after gettings rep ids
this.loadSubtitles();
+ this.loadAlternateAudio();
this.showPlayButton();
this.loaded = true;
@@ -402,14 +450,24 @@ class DashViewer extends VideoBaseViewer {
// Iterate over all available video representations and find the one that
// seems the biggest so that the video player is set to the max size
- const hdRepresentation = tracks.reduce((a, b) => (a.width > b.width ? a : b));
- const sdRepresentation = tracks.reduce((a, b) => (a.width < b.width ? a : b));
+ const hdRep = tracks.reduce((a, b) => (a.width > b.width ? a : b));
+ const sdRep = tracks.reduce((a, b) => (a.width < b.width ? a : b));
+ if (this.player.isAudioOnly()) {
+ // There is only audio, no video
+ this.videoWidth = DEFAULT_VIDEO_WIDTH_PX;
+ this.videoHeight = DEFAULT_VIDEO_HEIGHT_PX;
+ } else {
+ this.videoWidth = hdRep.width;
+ this.videoHeight = hdRep.height;
+ this.sdVideoId = sdRep.videoId;
+
+ // If there is an HD representation separate from the SD
+ if (hdRep.videoId !== sdRep.videoId) {
+ this.hdVideoId = hdRep.videoId;
+ }
+ }
- this.videoWidth = hdRepresentation.width;
- this.videoHeight = hdRepresentation.height;
this.aspect = this.videoWidth / this.videoHeight;
- this.hdRepresentation = hdRepresentation;
- this.sdRepresentation = sdRepresentation;
}
/**
diff --git a/src/lib/viewers/media/MediaControls.js b/src/lib/viewers/media/MediaControls.js
index 1ac15006c..69029ba1a 100644
--- a/src/lib/viewers/media/MediaControls.js
+++ b/src/lib/viewers/media/MediaControls.js
@@ -179,6 +179,16 @@ class MediaControls extends EventEmitter {
this.emit('subtitlechange');
}
+ /**
+ * Audio-track handler
+ *
+ * @private
+ * @return {void}
+ */
+ handleAudioTrack() {
+ this.emit('audiochange');
+ }
+
/**
* Attaches settings menu
*
@@ -877,6 +887,17 @@ class MediaControls extends EventEmitter {
this.settings.addListener('subtitles', this.handleSubtitle);
this.settings.loadSubtitles(subtitles, language);
}
+
+ /**
+ * Takes a list of audio tracks and populates the settings menu
+ *
+ * @param {Array} audioLanguages - An ordered list of languages of the audio tracks
+ * @return {void}
+ */
+ initAlternateAudio(audioLanguages) {
+ this.settings.addListener('audiotracks', this.handleAudioTrack);
+ this.settings.loadAlternateAudio(audioLanguages);
+ }
}
export default MediaControls;
diff --git a/src/lib/viewers/media/Settings.js b/src/lib/viewers/media/Settings.js
index 81bd3afa0..16204fc92 100644
--- a/src/lib/viewers/media/Settings.js
+++ b/src/lib/viewers/media/Settings.js
@@ -10,6 +10,7 @@ const CLASS_SETTINGS = 'bp-media-settings';
const CLASS_SETTINGS_SELECTED = 'bp-media-settings-selected';
const CLASS_SETTINGS_OPEN = 'bp-media-settings-is-open';
const CLASS_SETTINGS_SUBTITLES_UNAVAILABLE = 'bp-media-settings-subtitles-unavailable';
+const CLASS_SETTINGS_AUDIOTRACKS_UNAVAILABLE = 'bp-media-settings-audiotracks-unavailable';
const CLASS_SETTINGS_SUBTITLES_ON = 'bp-media-settings-subtitles-on';
const SELECTOR_SETTINGS_SUB_ITEM = '.bp-media-settings-sub-item';
const SELECTOR_SETTINGS_VALUE = '.bp-media-settings-value';
@@ -32,6 +33,11 @@ const SETTINGS_TEMPLATE = `
+const SUBMENU_SUBITEM_TEMPLATE = `
`;
@@ -147,6 +159,7 @@ class Settings extends EventEmitter {
addActivationListener(this.settingsEl, this.menuEventHandler);
this.containerEl.classList.add(CLASS_SETTINGS_SUBTITLES_UNAVAILABLE);
+ this.containerEl.classList.add(CLASS_SETTINGS_AUDIOTRACKS_UNAVAILABLE);
this.init();
}
@@ -319,7 +332,8 @@ class Settings extends EventEmitter {
} else if (event.type === 'keydown') {
const key = decodeKeydown(event).toLowerCase();
const menuEl = menuItem.parentElement;
- const itemIdx = [].findIndex.call(menuEl.children, (e) => {
+ const visibleOptions = [].filter.call(menuEl.children, (option) => option.offsetParent !== null);
+ const itemIdx = [].findIndex.call(visibleOptions, (e) => {
return e.contains(menuItem);
});
@@ -333,21 +347,21 @@ class Settings extends EventEmitter {
case 'arrowup': {
this.containerEl.classList.add(CLASS_ELEM_KEYBOARD_FOCUS);
if (itemIdx > 0) {
- const newNode = menuEl.children[itemIdx - 1];
+ const newNode = visibleOptions[itemIdx - 1];
newNode.focus();
}
break;
}
case 'arrowdown': {
this.containerEl.classList.add(CLASS_ELEM_KEYBOARD_FOCUS);
- if (itemIdx >= 0 && itemIdx < menuEl.children.length - 1) {
- const newNode = menuEl.children[itemIdx + 1];
+ if (itemIdx >= 0 && itemIdx < visibleOptions.length - 1) {
+ const newNode = visibleOptions[itemIdx + 1];
newNode.focus();
}
break;
}
case 'arrowleft': {
- if (itemIdx >= 0 && !menuEl.children[itemIdx].classList.contains('bp-media-settings-item')) {
+ if (itemIdx >= 0 && !visibleOptions[itemIdx].classList.contains('bp-media-settings-item')) {
// Go back to the main menu
this.reset();
this.firstMenuItem.focus();
@@ -356,7 +370,7 @@ class Settings extends EventEmitter {
}
case 'arrowright': {
if (itemIdx >= 0) {
- const curNode = menuEl.children[itemIdx];
+ const curNode = visibleOptions[itemIdx];
const dataType = curNode.getAttribute('data-type');
if (curNode.classList.contains('bp-media-settings-item') && dataType !== 'menu') {
this.showSubMenu(dataType);
@@ -422,8 +436,11 @@ class Settings extends EventEmitter {
// Remove the checkmark from the prior selected option in the sub menu
const prevSelected = this.getSelectedOption(type);
- prevSelected.classList.remove(CLASS_SETTINGS_SELECTED);
- prevSelected.removeAttribute('aria-checked');
+ if (prevSelected) {
+ // this may not exist, for instance, when first initializing audio tracks
+ prevSelected.classList.remove(CLASS_SETTINGS_SELECTED);
+ prevSelected.removeAttribute('aria-checked');
+ }
// Add a checkmark to the new selected option in the sub menu
option.classList.add(CLASS_SETTINGS_SELECTED);
@@ -588,7 +605,10 @@ class Settings extends EventEmitter {
this.subtitles = subtitles;
this.language = language;
this.subtitles.forEach((subtitle, idx) => {
- insertTemplate(subtitlesSubMenu, SUBTITLES_SUBITEM_TEMPLATE.replace(/{{dataValue}}/g, idx));
+ insertTemplate(
+ subtitlesSubMenu,
+ SUBMENU_SUBITEM_TEMPLATE.replace(/{{dataType}}/g, 'subtitles').replace(/{{dataValue}}/g, idx)
+ );
const languageNode = subtitlesSubMenu.lastChild.querySelector('.bp-media-settings-value');
languageNode.textContent = subtitle;
});
@@ -602,6 +622,34 @@ class Settings extends EventEmitter {
this.reset();
}
+
+ /**
+ * Takes an ordered list of audio languages and populates the settings menu
+ *
+ * @param {Array} audioLanguages - An ordered list of languages of the audio tracks
+ * @return {void}
+ */
+ loadAlternateAudio(audioLanguages) {
+ const audioTracksSubMenu = this.settingsEl.querySelector('.bp-media-settings-menu-audiotracks');
+ audioLanguages.forEach((language, idx) => {
+ insertTemplate(
+ audioTracksSubMenu,
+ SUBMENU_SUBITEM_TEMPLATE.replace(/{{dataType}}/g, 'audiotracks').replace(/{{dataValue}}/g, idx)
+ );
+ const trackNode = audioTracksSubMenu.lastChild.querySelector('.bp-media-settings-value');
+ // It's common for the language to be unknown and show up as "und" language code. Just omit
+ // the language in this case, otherwise display the language too
+ let textContent = `${__('track')} ${idx + 1}`;
+ if (language !== 'und') {
+ textContent = `${textContent} (${language})`;
+ }
+ trackNode.textContent = textContent;
+ });
+ this.chooseOption('audiotracks', '0');
+
+ this.containerEl.classList.remove(CLASS_SETTINGS_AUDIOTRACKS_UNAVAILABLE);
+ this.reset();
+ }
}
export default Settings;
diff --git a/src/lib/viewers/media/Settings.scss b/src/lib/viewers/media/Settings.scss
index e813e3fa3..c459c60f9 100644
--- a/src/lib/viewers/media/Settings.scss
+++ b/src/lib/viewers/media/Settings.scss
@@ -39,15 +39,18 @@ $item-hover-color: #f6fafd;
.bp-media-settings-menu-quality,
.bp-media-settings-menu-speed,
.bp-media-settings-menu-subtitles,
+.bp-media-settings-menu-audiotracks,
.bp-media-settings-show-speed .bp-media-settings-menu-main,
.bp-media-settings-show-quality .bp-media-settings-menu-main,
-.bp-media-settings-show-subtitles .bp-media-settings-menu-main {
+.bp-media-settings-show-subtitles .bp-media-settings-menu-main,
+.bp-media-settings-show-audiotracks .bp-media-settings-menu-main {
display: none;
}
.bp-media-settings-show-speed .bp-media-settings-menu-speed,
.bp-media-settings-show-quality .bp-media-settings-menu-quality,
-.bp-media-settings-show-subtitles .bp-media-settings-menu-subtitles {
+.bp-media-settings-show-subtitles .bp-media-settings-menu-subtitles,
+.bp-media-settings-show-audiotracks .bp-media-settings-menu-audiotracks {
display: table;
}
@@ -55,7 +58,8 @@ $item-hover-color: #f6fafd;
.bp-media-mp4,
.bp-media-mp3 {
.bp-media-settings-item-quality,
- .bp-media-settings-item-subtitles {
+ .bp-media-settings-item-subtitles,
+ .bp-media-settings-item-audiotracks {
display: none;
}
}
@@ -103,6 +107,13 @@ $item-hover-color: #f6fafd;
}
}
+.bp-media-settings-item-audiotracks,
+.bp-media-settings-menu-audiotracks {
+ .bp-media-settings-audiotracks-unavailable & {
+ display: none;
+ }
+}
+
.bp-media-settings-label,
.bp-media-settings-value {
display: table-cell;
diff --git a/src/lib/viewers/media/__tests__/DashViewer-test.js b/src/lib/viewers/media/__tests__/DashViewer-test.js
index 4def90693..f751198a1 100644
--- a/src/lib/viewers/media/__tests__/DashViewer-test.js
+++ b/src/lib/viewers/media/__tests__/DashViewer-test.js
@@ -65,9 +65,11 @@ describe('lib/viewers/media/DashViewer', () => {
getStats: () => {},
getTextTracks: () => {},
getVariantTracks: () => {},
+ isAudioOnly: () => {},
load: () => {},
selectTextTrack: () => {},
selectVariantTrack: () => {},
+ selectAudioLanguage: () => {},
setTextTrackVisibility: () => {}
};
stubs.mockPlayer = sandbox.mock(dash.player);
@@ -77,6 +79,7 @@ describe('lib/viewers/media/DashViewer', () => {
destroy: () => {},
initFilmstrip: () => {},
initSubtitles: () => {},
+ initAlternateAudio: () => {},
removeAllListeners: () => {},
removeListener: () => {},
show: sandbox.stub()
@@ -105,8 +108,8 @@ describe('lib/viewers/media/DashViewer', () => {
it('should set up dash element', () => {
expect(dash.bandwidthHistory).to.deep.equal([]);
expect(dash.switchHistory).to.deep.equal([]);
- expect(dash.hdRepresentation).to.deep.equal({});
- expect(dash.sdRepresentation).to.deep.equal({});
+ expect(dash.hdVideoId).to.equal(-1);
+ expect(dash.sdVideoId).to.equal(-1);
expect(dash.wrapperEl).to.have.class(CSS_CLASS_MEDIA);
});
});
@@ -241,23 +244,54 @@ describe('lib/viewers/media/DashViewer', () => {
});
});
- describe('enableHD()', () => {
- it('should enable HD video for the file', () => {
- dash.hdRepresentation = { id: '1' };
+ describe('enableVideoId()', () => {
+ it('should enable videoId while maintaining the same audio', () => {
+ const variant1 = { id: 1, videoId: 1, audioId: 5, active: false };
+ const variant2 = { id: 2, videoId: 2, audioId: 5, active: false };
+ const variant3 = { id: 3, videoId: 1, audioId: 6, active: false };
+ const variant4 = { id: 4, videoId: 2, audioId: 6, active: true };
+ const variant5 = { id: 5, videoId: 1, audioId: 7, active: false };
+ const variant6 = { id: 6, videoId: 2, audioId: 7, active: false };
+ stubs.mockPlayer.expects('getVariantTracks').returns([
+ variant1, variant2, variant3, variant4, variant5, variant6
+ ]);
+ sandbox.stub(dash, 'getActiveTrack').returns(variant4);
sandbox.stub(dash, 'showLoadingIcon');
- stubs.mockPlayer.expects('selectVariantTrack').withArgs(dash.hdRepresentation, true);
- dash.enableHD();
- expect(dash.showLoadingIcon).to.be.calledWith('1');
+ stubs.mockPlayer.expects('selectVariantTrack').withArgs(variant3, true);
+
+ dash.enableVideoId(1);
+
+ expect(dash.showLoadingIcon).to.be.calledWith(3);
});
- });
- describe('enableSD()', () => {
- it('should enable SD video for the file', () => {
- dash.sdRepresentation = { id: '1' };
+ it('should do nothing if enabling a videoId which is already active', () => {
+ const variant1 = { id: 1, videoId: 1, audioId: 5, active: false };
+ const variant2 = { id: 2, videoId: 2, audioId: 5, active: true };
+ stubs.mockPlayer.expects('getVariantTracks').returns([
+ variant1, variant2
+ ]);
+ sandbox.stub(dash, 'getActiveTrack').returns(variant2);
sandbox.stub(dash, 'showLoadingIcon');
- stubs.mockPlayer.expects('selectVariantTrack').withArgs(dash.sdRepresentation, true);
- dash.enableSD();
- expect(dash.showLoadingIcon).to.be.calledWith('1');
+ stubs.mockPlayer.expects('selectVariantTrack').never();
+
+ dash.enableVideoId(2);
+
+ expect(dash.showLoadingIcon).to.not.be.called;
+ });
+
+ it('should do nothing if enabling an invalid videoId', () => {
+ const variant1 = { id: 1, videoId: 1, audioId: 5, active: false };
+ const variant2 = { id: 2, videoId: 2, audioId: 5, active: true };
+ stubs.mockPlayer.expects('getVariantTracks').returns([
+ variant1, variant2
+ ]);
+ sandbox.stub(dash, 'getActiveTrack').returns(variant2);
+ sandbox.stub(dash, 'showLoadingIcon');
+ stubs.mockPlayer.expects('selectVariantTrack').never();
+
+ dash.enableVideoId(-1);
+
+ expect(dash.showLoadingIcon).to.not.be.called;
});
});
@@ -275,8 +309,9 @@ describe('lib/viewers/media/DashViewer', () => {
describe('handleQuality()', () => {
beforeEach(() => {
- stubs.hd = sandbox.stub(dash, 'enableHD');
- stubs.sd = sandbox.stub(dash, 'enableSD');
+ dash.hdVideoId = 1;
+ dash.sdVideoId = 2;
+ stubs.enableVideoId = sandbox.stub(dash, 'enableVideoId');
stubs.adapt = sandbox.stub(dash, 'enableAdaptation');
});
@@ -284,7 +319,7 @@ describe('lib/viewers/media/DashViewer', () => {
sandbox.stub(dash.cache, 'get').returns('hd');
dash.handleQuality();
expect(stubs.adapt).to.be.calledWith(false);
- expect(stubs.hd).to.be.called;
+ expect(stubs.enableVideoId).to.be.calledWith(dash.hdVideoId);
expect(dash.emit).to.be.calledWith('qualitychange', 'hd');
});
@@ -292,7 +327,7 @@ describe('lib/viewers/media/DashViewer', () => {
sandbox.stub(dash.cache, 'get').returns('sd');
dash.handleQuality();
expect(stubs.adapt).to.be.calledWith(false);
- expect(stubs.sd).to.be.called;
+ expect(stubs.enableVideoId).to.be.calledWith(dash.sdVideoId);
expect(dash.emit).to.be.calledWith('qualitychange', 'sd');
});
@@ -313,11 +348,11 @@ describe('lib/viewers/media/DashViewer', () => {
describe('adaptationHandler()', () => {
beforeEach(() => {
- stubs.active = { id: 1, bandwidth: 'bandwidth' };
+ stubs.active = { id: 1, bandwidth: 'bandwidth', videoId: 1 };
stubs.getActive = sandbox.stub(dash, 'getActiveTrack').returns(stubs.active);
stubs.loaded = sandbox.stub(dash, 'isLoaded').returns(true);
stubs.hide = sandbox.stub(dash, 'hideLoadingIcon');
- dash.hdRepresentation = { id: 1 };
+ dash.hdVideoId = 1;
dash.adapting = false;
});
@@ -328,7 +363,7 @@ describe('lib/viewers/media/DashViewer', () => {
});
it('should handle change from HD resolution', () => {
- stubs.getActive.returns({ id: 2 });
+ stubs.getActive.returns({ id: 2, videoId: 2 });
dash.wrapperEl.classList.add(CSS_CLASS_HD);
dash.adaptationHandler();
expect(dash.wrapperEl).to.not.have.class(CSS_CLASS_HD);
@@ -401,6 +436,7 @@ describe('lib/viewers/media/DashViewer', () => {
});
stubs.mockControls.expects('addListener').withArgs('qualitychange', sinon.match.func);
stubs.mockControls.expects('addListener').withArgs('subtitlechange', sinon.match.func);
+ stubs.mockControls.expects('addListener').withArgs('audiochange', sinon.match.func);
dash.addEventListenersForMediaControls();
});
});
@@ -419,6 +455,7 @@ describe('lib/viewers/media/DashViewer', () => {
sandbox.stub(dash, 'calculateVideoDimensions');
sandbox.stub(dash, 'loadUI');
sandbox.stub(dash, 'loadFilmStrip');
+ sandbox.stub(dash, 'loadAlternateAudio');
sandbox.stub(dash, 'resize');
sandbox.stub(dash, 'handleVolume');
sandbox.stub(dash, 'startBandwidthTracking');
@@ -430,6 +467,7 @@ describe('lib/viewers/media/DashViewer', () => {
expect(dash.showMedia).to.be.called;
expect(dash.showPlayButton).to.be.called;
expect(dash.loadSubtitles).to.be.called;
+ expect(dash.loadAlternateAudio).to.be.called;
expect(dash.emit).to.be.calledWith('load');
expect(dash.loaded).to.be.true;
expect(document.activeElement).to.equal(dash.mediaContainerEl);
@@ -558,6 +596,55 @@ describe('lib/viewers/media/DashViewer', () => {
});
});
+ describe('loadAlternateAudio()', () => {
+ it('should select unique audio tracks', () => {
+ const variant1 = { videoId: 0, audioId: 0, language: 'eng', roles: ['audio0']};
+ const variant2 = { videoId: 1, audioId: 0, language: 'eng', roles: ['audio0']};
+ const variant3 = { videoId: 0, audioId: 1, language: 'rus', roles: ['audio1']};
+ const variant4 = { videoId: 1, audioId: 1, language: 'rus', roles: ['audio1']};
+ const variant5 = { videoId: 2, audioId: 1, language: 'rus', roles: ['audio1']};
+ const allVariants = [variant1, variant2, variant3, variant4, variant5];
+ stubs.mockPlayer.expects('getVariantTracks').returns(allVariants);
+ stubs.mockControls.expects('initAlternateAudio');
+
+ dash.loadAlternateAudio();
+
+ expect(dash.audioTracks).to.deep.equal([
+ { language: 'eng', role: 'audio0' },
+ { language: 'rus', role: 'audio1' }
+ ]);
+ });
+
+ it('should translate and initialize audio in sorted order', () => {
+ const variant1 = { videoId: 0, audioId: 0, language: 'eng', roles: ['audio0']};
+ const variant2 = { videoId: 0, audioId: 1, language: 'rus', roles: ['audio0']};
+ const variant3 = { videoId: 0, audioId: 2, language: 'spa', roles: ['audio0']};
+ const variant4 = { videoId: 0, audioId: 3, language: 'kor', roles: ['audio0']};
+ const variant5 = { videoId: 0, audioId: 4, language: 'fra', roles: ['audio0']};
+ const allVariants = [variant3, variant1, variant4, variant2, variant5];
+ stubs.mockPlayer.expects('getVariantTracks').returns(allVariants);
+ stubs.mockControls
+ .expects('initAlternateAudio')
+ .withArgs(['English', 'Russian', 'Spanish', 'Korean', 'French']);
+
+ dash.loadAlternateAudio();
+ });
+
+ it('should not initialize alternate audio if there is none', () => {
+ const variant1 = { videoId: 0, audioId: 0, language: 'eng', roles: ['audio0']};
+ const variant2 = { videoId: 1, audioId: 0, language: 'eng', roles: ['audio0']};
+ const allVariants = [variant1, variant2];
+ stubs.mockPlayer.expects('getVariantTracks').returns(allVariants);
+ stubs.mockControls.expects('initAlternateAudio').never();
+
+ dash.loadAlternateAudio();
+
+ expect(dash.audioTracks).to.deep.equal([
+ { language: 'eng', role: 'audio0' }
+ ]);
+ });
+ });
+
describe('handleSubtitle()', () => {
it('should select track from front of text track list', () => {
const english = { language: 'eng', id: 3 };
@@ -620,12 +707,67 @@ describe('lib/viewers/media/DashViewer', () => {
});
});
+ describe('handleAudioTrack()', () => {
+ it('should select correct audio', () => {
+ dash.audioTracks = [
+ { language: 'eng', role: 'audio0' },
+ { language: 'eng', role: 'audio1' },
+ { language: 'eng', role: 'audio2' }
+ ];
+ sandbox.stub(dash.cache, 'get').returns('1');
+ stubs.mockPlayer.expects('selectAudioLanguage').withArgs('eng', 'audio1');
+
+ dash.handleAudioTrack();
+ });
+
+ it('should not select audio if index out of bounds', () => {
+ dash.audioTracks = [
+ { language: 'eng', role: 'audio0' },
+ { language: 'eng', role: 'audio1' },
+ { language: 'eng', role: 'audio2' }
+ ];
+ sandbox.stub(dash.cache, 'get').returns('3');
+ stubs.mockPlayer.expects('selectAudioLanguage').never();
+
+ dash.handleAudioTrack();
+ });
+ });
+
describe('calculateVideoDimensions()', () => {
it('should calculate the video dimensions based on the reps', () => {
- stubs.mockPlayer.expects('getVariantTracks').returns([{ width: 200 }, { width: 100 }]);
+ stubs.mockPlayer.expects('isAudioOnly').returns(false);
+ stubs.mockPlayer.expects('getVariantTracks').returns([
+ { width: 200, videoId: 1 },
+ { width: 100, videoId: 2 }
+ ]);
+ dash.calculateVideoDimensions();
+ expect(dash.hdVideoId).to.equal(1);
+ expect(dash.sdVideoId).to.equal(2);
+ expect(dash.videoWidth).to.equal(200);
+ });
+
+ it('should use SD video dimensions if no HD', () => {
+ stubs.mockPlayer.expects('isAudioOnly').returns(false);
+ stubs.mockPlayer.expects('getVariantTracks').returns([
+ { width: 640, videoId: 1, audioId: 2 },
+ { width: 640, videoId: 1, audioId: 3 }
+ ]);
+ dash.calculateVideoDimensions();
+ expect(dash.hdVideoId).to.equal(-1);
+ expect(dash.sdVideoId).to.equal(1);
+ expect(dash.videoWidth).to.equal(640);
+ });
+
+ it('should default video dimensions when video is audio-only', () => {
+ stubs.mockPlayer.expects('isAudioOnly').returns(true);
+ stubs.mockPlayer.expects('getVariantTracks').returns([
+ { width: null, videoId: null, audioId: 1 },
+ { width: null, videoId: null, audioId: 2 }
+ ]);
dash.calculateVideoDimensions();
- expect(dash.hdRepresentation.width).to.equal(200);
- expect(dash.sdRepresentation.width).to.equal(100);
+ expect(dash.hdVideoId).to.equal(-1);
+ expect(dash.sdVideoId).to.equal(-1);
+ expect(dash.videoWidth).to.equal(854); // default to width of 854 (480p)
});
});
diff --git a/src/lib/viewers/media/__tests__/MediaControls-test.js b/src/lib/viewers/media/__tests__/MediaControls-test.js
index 95d3183e3..6e883203b 100644
--- a/src/lib/viewers/media/__tests__/MediaControls-test.js
+++ b/src/lib/viewers/media/__tests__/MediaControls-test.js
@@ -1021,5 +1021,17 @@ describe('lib/viewers/media/MediaControls', () => {
expect(mediaControls.settings.loadSubtitles).to.be.calledWith(subs);
});
});
+
+ describe('initAlternateAudio()', () => {
+ it('should load alternate audio', () => {
+ sandbox.stub(mediaControls.settings, 'loadAlternateAudio');
+ const audios = [
+ { language: 'eng', role: 'audio0' },
+ { language: 'rus', role: 'audio1' }
+ ];
+ mediaControls.initAlternateAudio(audios);
+ expect(mediaControls.settings.loadAlternateAudio).to.be.calledWith(audios);
+ });
+ });
});
/* eslint-enable no-unused-expressions */
diff --git a/src/lib/viewers/media/__tests__/Settings-test.js b/src/lib/viewers/media/__tests__/Settings-test.js
index 9da27a0f4..023c5bda5 100644
--- a/src/lib/viewers/media/__tests__/Settings-test.js
+++ b/src/lib/viewers/media/__tests__/Settings-test.js
@@ -765,6 +765,51 @@ describe('lib/viewers/media/Settings', () => {
});
});
+ describe('loadAlternateAudio()', () => {
+ it('Should load all audio tracks and make them available', () => {
+ const audioMenu = settings.settingsEl.querySelector('.bp-media-settings-menu-audiotracks');
+ sandbox.stub(settings, 'chooseOption');
+
+ settings.loadAlternateAudio(['English', 'Russian', 'Spanish']);
+
+ expect(settings.chooseOption).to.be.calledWith('audiotracks', '0');
+ expect(audioMenu.children.length).to.equal(4); // Three languages, and back to main menu
+ expect(settings.containerEl).to.not.have.class('bp-media-settings-audiotracks-unavailable');
+ });
+
+ it('Should reset menu dimensions after loading', () => {
+ sandbox.stub(settings, 'setMenuContainerDimensions');
+
+ settings.loadAlternateAudio(['English', 'Russian', 'Spanish']);
+
+ expect(settings.setMenuContainerDimensions).to.be.calledWith(settings.settingsEl.firstChild);
+ });
+
+ it('Should not list language for "und" language code', () => {
+ const audioMenu = settings.settingsEl.querySelector('.bp-media-settings-menu-audiotracks');
+
+ settings.loadAlternateAudio(['English', 'und']);
+
+ const audio0 = audioMenu.querySelector('[data-value="0"]').querySelector('.bp-media-settings-value');
+ const audio1 = audioMenu.querySelector('[data-value="1"]').querySelector('.bp-media-settings-value');
+ expect(audio0.innerHTML).to.equal('Track 1 (English)');
+ expect(audio1.innerHTML).to.equal('Track 2');
+ });
+
+ it('Should escape audio languages and roles', () => {
+ const audioMenu = settings.settingsEl.querySelector('.bp-media-settings-menu-audiotracks');
+
+ // There shouldn't be a way to get such inputs into this method in normal use case anyway
+ // because it goes through multiple levels of sanitization, but just in case...
+ settings.loadAlternateAudio(['English', '
']);
+
+ const audio0 = audioMenu.querySelector('[data-value="0"]').querySelector('.bp-media-settings-value');
+ const audio1 = audioMenu.querySelector('[data-value="1"]').querySelector('.bp-media-settings-value');
+ expect(audio0.innerHTML).to.equal('Track 1 (English)');
+ expect(audio1.innerHTML).to.equal('Track 2 (<badboy>)');
+ });
+ });
+
describe('hasSubtitles()', () => {
it('Should be false before loading subtitles', () => {
expect(settings.hasSubtitles()).to.be.false;