diff --git a/scripts/sources.json b/scripts/sources.json index e4a1a4a8a..ed05a20ba 100644 --- a/scripts/sources.json +++ b/scripts/sources.json @@ -357,5 +357,17 @@ "uri": "https://d2zihajmogu5jn.cloudfront.net/pdt-test-source/endlist.m3u8", "mimetype": "application/x-mpegurl", "features": [] + }, + { + "name": "audio only dash, two groups", + "uri": "https://d2zihajmogu5jn.cloudfront.net/audio-only-dash/dash.mpd", + "mimetype": "application/dash+xml", + "features": [] + }, + { + "name": "video only dash, two renditions", + "uri": "https://d2zihajmogu5jn.cloudfront.net/video-only-dash/dash.mpd", + "mimetype": "application/dash+xml", + "features": [] } ] diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index f32624e1c..7973a01af 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -342,6 +342,65 @@ export class MasterPlaylistController extends videojs.EventTarget { this.abrTimer_ = null; } + /** + * Get a list of playlists for the currently selected audio playlist + * + * @return {Array} the array of audio playlists + */ + getAudioTrackPlaylists_() { + const master = this.master(); + + // if we don't have any audio groups then we can only + // assume that the audio tracks are contained in masters + // playlist array, use that or an empty array. + if (!master || !master.mediaGroups || !master.mediaGroups.AUDIO) { + return master && master.playlists || []; + } + + const AUDIO = master.mediaGroups.AUDIO; + const groupKeys = Object.keys(AUDIO); + let track; + + // get the current active track + if (Object.keys(this.mediaTypes_.AUDIO.groups).length) { + track = this.mediaTypes_.AUDIO.activeTrack(); + // or get the default track from master if mediaTypes_ isn't setup yet + } else { + // default group is `main` or just the first group. + const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]]; + + for (const label in defaultGroup) { + if (defaultGroup[label].default) { + track = {label}; + break; + } + } + } + + // no active track no playlists. + if (!track) { + return []; + } + + const playlists = []; + + // get all of the playlists that are possible for the + // active track. + for (const group in AUDIO) { + if (AUDIO[group][track.label]) { + const properties = AUDIO[group][track.label]; + + if (properties.playlists) { + playlists.push.apply(playlists, properties.playlists); + } else { + playlists.push(properties); + } + } + } + + return playlists; + } + /** * Register event handlers on the master playlist loader. A helper * function for construction time. diff --git a/src/media-groups.js b/src/media-groups.js index 63de67aec..eb920abcf 100644 --- a/src/media-groups.js +++ b/src/media-groups.js @@ -2,6 +2,8 @@ import videojs from 'video.js'; import PlaylistLoader from './playlist-loader'; import DashPlaylistLoader from './dash-playlist-loader'; import noop from './util/noop'; +import {isAudioOnly} from './playlist.js'; +import logger from './util/logger'; /** * Convert the properties of an HLS track into an audioTrackKind. @@ -78,13 +80,22 @@ export const onGroupChanged = (type, settings) => () => { mediaTypes: { [type]: mediaType } } = settings; const activeTrack = mediaType.activeTrack(); - const activeGroup = mediaType.activeGroup(activeTrack); + const activeGroup = mediaType.getActiveGroup(); const previousActiveLoader = mediaType.activePlaylistLoader; + const lastGroup = mediaType.lastGroup_; + + // the group did not change do nothing + if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) { + return; + } + + mediaType.lastGroup_ = activeGroup; + mediaType.lastTrack_ = activeTrack; stopLoaders(segmentLoader, mediaType); - if (!activeGroup) { - // there is no group active + if (!activeGroup || activeGroup.isMasterPlaylist) { + // there is no group active or active group is a main playlist and won't change return; } @@ -109,9 +120,12 @@ export const onGroupChanging = (type, settings) => () => { const { segmentLoaders: { [type]: segmentLoader - } + }, + mediaTypes: { [type]: mediaType } } = settings; + mediaType.lastGroup_ = null; + segmentLoader.abort(); segmentLoader.pause(); }; @@ -132,6 +146,7 @@ export const onGroupChanging = (type, settings) => () => { */ export const onTrackChanged = (type, settings) => () => { const { + masterPlaylistLoader, segmentLoaders: { [type]: segmentLoader, main: mainSegmentLoader @@ -139,8 +154,17 @@ export const onTrackChanged = (type, settings) => () => { mediaTypes: { [type]: mediaType } } = settings; const activeTrack = mediaType.activeTrack(); - const activeGroup = mediaType.activeGroup(activeTrack); + const activeGroup = mediaType.getActiveGroup(); const previousActiveLoader = mediaType.activePlaylistLoader; + const lastTrack = mediaType.lastTrack_; + + // track did not change, do nothing + if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) { + return; + } + + mediaType.lastGroup_ = activeGroup; + mediaType.lastTrack_ = activeTrack; stopLoaders(segmentLoader, mediaType); @@ -149,6 +173,28 @@ export const onTrackChanged = (type, settings) => () => { return; } + if (activeGroup.isMasterPlaylist) { + // track did not change, do nothing + if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) { + return; + } + + const mpc = settings.vhs.masterPlaylistController_; + const newPlaylist = mpc.selectPlaylist(); + + // media will not change do nothing + if (mpc.media() === newPlaylist) { + return; + } + + mediaType.logger_(`track change. Switching master audio from ${lastTrack.id} to ${activeTrack.id}`); + masterPlaylistLoader.pause(); + mainSegmentLoader.resetEverything(); + mpc.fastQualityChange_(newPlaylist); + + return; + } + if (type === 'AUDIO') { if (!activeGroup.playlistLoader) { // when switching from demuxed audio/video to muxed audio/video (noted by no @@ -375,16 +421,19 @@ export const initialize = { sourceType, segmentLoaders: { [type]: segmentLoader }, requestOptions, - master: { mediaGroups, playlists }, + master: {mediaGroups}, mediaTypes: { [type]: { groups, - tracks + tracks, + logger_ } }, masterPlaylistLoader } = settings; + const audioOnlyMaster = isAudioOnly(masterPlaylistLoader.master); + // force a default if we have none if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { @@ -395,36 +444,19 @@ export const initialize = { if (!groups[groupId]) { groups[groupId] = []; } - - // List of playlists that have an AUDIO attribute value matching the current - // group ID - const groupPlaylists = playlists.filter(playlist => { - return playlist.attributes[type] === groupId; - }); - for (const variantLabel in mediaGroups[type][groupId]) { let properties = mediaGroups[type][groupId][variantLabel]; - // List of playlists for the current group ID that do not have a matching uri - // with this alternate audio variant - const unmatchingPlaylists = groupPlaylists.filter(playlist => { - return playlist.resolvedUri !== properties.resolvedUri; - }); - - // If there are no playlists using this audio group other than ones - // that match it's uri, then the playlist is audio only. We delete the resolvedUri - // property here to prevent a playlist loader from being created so that we don't have - // both the main and audio segment loaders loading the same audio segments - // from the same playlist. - if (!unmatchingPlaylists.length && groupPlaylists.length) { - delete properties.resolvedUri; - } - let playlistLoader; - // if vhs-json was provided as the source, and the media playlist was resolved, - // use the resolved media playlist object - if (sourceType === 'vhs-json' && properties.playlists) { + if (audioOnlyMaster) { + logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a master playlist`); + properties.isMasterPlaylist = true; + playlistLoader = null; + + // if vhs-json was provided as the source, and the media playlist was resolved, + // use the resolved media playlist object + } else if (sourceType === 'vhs-json' && properties.playlists) { playlistLoader = new PlaylistLoader( properties.playlists[0], vhs, @@ -658,17 +690,28 @@ export const activeGroup = (type, settings) => (track) => { let variants = null; + // set to variants to main media active group if (media.attributes[type]) { variants = groups[media.attributes[type]]; } - variants = variants || groups.main; + const groupKeys = Object.keys(groups); + + if (!variants) { + // use the main group if it exists + if (groups.main) { + variants = groups.main; + // only one group, use that one + } else if (groupKeys.length === 1) { + variants = groups[groupKeys[0]]; + } + } if (typeof track === 'undefined') { return variants; } - if (track === null) { + if (track === null || !variants) { // An active track was specified so a corresponding group is expected. track === null // means no track is currently active so there is no corresponding group return null; @@ -726,6 +769,16 @@ export const activeTrack = { } }; +export const getActiveGroup = (type, {mediaTypes}) => () => { + const activeTrack_ = mediaTypes[type].activeTrack(); + + if (!activeTrack_) { + return null; + } + + return mediaTypes[type].activeGroup(activeTrack_); +}; + /** * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, * Closed-Captions) specified in the master manifest. @@ -767,6 +820,7 @@ export const setupMediaGroups = (settings) => { mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); mediaTypes[type].onGroupChanging = onGroupChanging(type, settings); mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); + mediaTypes[type].getActiveGroup = getActiveGroup(type, settings); }); // DO NOT enable the default subtitle or caption track. @@ -777,6 +831,7 @@ export const setupMediaGroups = (settings) => { const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id; mediaTypes.AUDIO.tracks[groupId].enabled = true; + mediaTypes.AUDIO.onGroupChanged(); mediaTypes.AUDIO.onTrackChanged(); } @@ -835,8 +890,11 @@ export const createMediaTypes = () => { activePlaylistLoader: null, activeGroup: noop, activeTrack: noop, + getActiveGroup: noop, onGroupChanged: noop, - onTrackChanged: noop + onTrackChanged: noop, + lastTrack_: null, + logger_: logger(`MediaGroups[${type}]`) }; }); diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 70d339949..542c543b1 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -143,6 +143,8 @@ export const comparePlaylistResolution = function(left, right) { * Current height of the player element (should account for the device pixel ratio) * @param {boolean} limitRenditionByPlayerDimensions * True if the player width and height should be used during the selection, false otherwise + * @param {Object} masterPlaylistController + * the current masterPlaylistController object * @return {Playlist} the highest bitrate playlist less than the * currently detected bandwidth, accounting for some amount of * bandwidth variance @@ -152,7 +154,8 @@ export const simpleSelector = function( playerBandwidth, playerWidth, playerHeight, - limitRenditionByPlayerDimensions + limitRenditionByPlayerDimensions, + masterPlaylistController ) { // If we end up getting called before `master` is available, exit early @@ -166,13 +169,23 @@ export const simpleSelector = function( height: playerHeight, limitRenditionByPlayerDimensions }; + + let playlists = master.playlists; + + // if playlist is audio only, select between currently active audio group playlists. + if (Playlist.isAudioOnly(master)) { + playlists = masterPlaylistController.getAudioTrackPlaylists_(); + // add audioOnly to options so that we log audioOnly: true + // at the buttom of this function for debugging. + options.audioOnly = true; + } // convert the playlists to an intermediary representation to make comparisons easier - let sortedPlaylistReps = master.playlists.map((playlist) => { + let sortedPlaylistReps = playlists.map((playlist) => { let bandwidth; - const width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; - const height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; + const width = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; + const height = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; - bandwidth = playlist.attributes.BANDWIDTH; + bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH; bandwidth = bandwidth || window.Number.MAX_VALUE; @@ -320,7 +333,8 @@ export const lastBandwidthSelector = function() { this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, - this.limitRenditionByPlayerDimensions + this.limitRenditionByPlayerDimensions, + this.masterPlaylistController_ ); }; @@ -358,7 +372,8 @@ export const movingAverageBandwidthSelector = function(decay) { average, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, - this.limitRenditionByPlayerDimensions + this.limitRenditionByPlayerDimensions, + this.masterPlaylistController_ ); }; }; diff --git a/src/playlist.js b/src/playlist.js index 5d9bb63a0..dd4eb0784 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -6,6 +6,7 @@ import videojs from 'video.js'; import window from 'global/window'; import {TIME_FUDGE_FACTOR} from './ranges.js'; +import {isAudioCodec} from '@videojs/vhs-utils/es/codecs.js'; const {createTimeRange} = videojs; @@ -563,6 +564,100 @@ export const isLowestEnabledRendition = (master, media) => { }).length === 0); }; +export const playlistMatch = (a, b) => { + // both playlits are null + // or only one playlist is non-null + // no match + if (!a && !b || (!a && b) || (a && !b)) { + return false; + } + + // playlist objects are the same, match + if (a === b) { + return true; + } + + // first try to use id as it should be the most + // accurate + if (a.id && b.id && a.id === b.id) { + return true; + } + + // next try to use reslovedUri as it should be the + // second most accurate. + if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) { + return true; + } + + // finally try to use uri as it should be accurate + // but might miss a few cases for relative uris + if (a.uri && b.uri && a.uri === b.uri) { + return true; + } + + return false; +}; + +const someAudioVariant = function(master, callback) { + const AUDIO = master && master.mediaGroups && master.mediaGroups.AUDIO || {}; + let found = false; + + for (const groupName in AUDIO) { + for (const label in AUDIO[groupName]) { + found = callback(AUDIO[groupName][label]); + + if (found) { + break; + } + } + + if (found) { + break; + } + } + + return !!found; +}; + +export const isAudioOnly = (master) => { + // we are audio only if we have no main playlists but do + // have media group playlists. + if (!master || !master.playlists || !master.playlists.length) { + // without audio variants or playlists this + // is not an audio only master. + const found = someAudioVariant(master, (variant) => + (variant.playlists && variant.playlists.length) || variant.uri); + + return found; + } + + // if every playlist has only an audio codec it is audio only + for (let i = 0; i < master.playlists.length; i++) { + const playlist = master.playlists[i]; + const CODECS = playlist.attributes && playlist.attributes.CODECS; + + // all codecs are audio, this is an audio playlist. + if (CODECS && CODECS.split(',').every((c) => isAudioCodec(c))) { + continue; + } + + // playlist is in an audio group it is audio only + const found = someAudioVariant(master, (variant) => playlistMatch(playlist, variant)); + + if (found) { + continue; + } + + // if we make it here this playlist isn't audio and we + // are not audio only + return false; + } + + // if we make it past every playlist without returning, then + // this is an audio only playlist. + return true; +}; + // exports export default { liveEdgeDelay, @@ -577,5 +672,7 @@ export default { isAes, hasAttribute, estimateSegmentRequestTime, - isLowestEnabledRendition + isLowestEnabledRendition, + isAudioOnly, + playlistMatch }; diff --git a/src/rendition-mixin.js b/src/rendition-mixin.js index c645365bf..26867dab3 100644 --- a/src/rendition-mixin.js +++ b/src/rendition-mixin.js @@ -1,4 +1,4 @@ -import { isIncompatible, isEnabled } from './playlist.js'; +import { isIncompatible, isEnabled, isAudioOnly } from './playlist.js'; import { codecsForPlaylist } from './util/codecs.js'; /** @@ -58,14 +58,14 @@ class Representation { const qualityChangeFunction = mpc[`${changeType}QualityChange_`].bind(mpc); // some playlist attributes are optional - if (playlist.attributes.RESOLUTION) { + if (playlist.attributes) { const resolution = playlist.attributes.RESOLUTION; - this.width = resolution.width; - this.height = resolution.height; - } + this.width = resolution && resolution.width; + this.height = resolution && resolution.height; - this.bandwidth = playlist.attributes.BANDWIDTH; + this.bandwidth = playlist.attributes.BANDWIDTH; + } this.codecs = codecsForPlaylist(mpc.master(), playlist); @@ -93,16 +93,18 @@ class Representation { * representation API into */ const renditionSelectionMixin = function(vhsHandler) { - const playlists = vhsHandler.playlists; // Add a single API-specific function to the VhsHandler instance vhsHandler.representations = () => { - if (!playlists || !playlists.master || !playlists.master.playlists) { + const master = vhsHandler.masterPlaylistController_.master(); + const playlists = isAudioOnly(master) ? + vhsHandler.masterPlaylistController_.getAudioTrackPlaylists_() : + master.playlists; + + if (!playlists) { return []; } return playlists - .master - .playlists .filter((media) => !isIncompatible(media)) .map((e, i) => new Representation(vhsHandler, e, e.id)); }; diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index dd5a92e70..b12705ecf 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -107,6 +107,75 @@ const sharedHooks = { QUnit.module('MasterPlaylistController', sharedHooks); +QUnit.test('getAudioTrackPlaylists_', function(assert) { + const mpc = this.masterPlaylistController; + const master = {playlists: [{uri: 'testing'}]}; + + mpc.master = () => master; + + assert.deepEqual( + mpc.getAudioTrackPlaylists_(), + master.playlists, + 'no media groups, return main playlists' + ); + + master.mediaGroups = { + AUDIO: { + main: { + en: {default: true, label: 'en', playlists: [{uri: 'foo'}, {uri: 'bar'}]}, + fr: {label: 'fr', playlists: [{uri: 'foo-fr'}, {uri: 'bar-fr'}]} + }, + alt: { + en: {default: true, label: 'en', playlists: [{uri: 'fizz'}, {uri: 'bazz'}]}, + fr: {label: 'fr', playlists: [{uri: 'fizz-fr'}, {uri: 'bazz-fr'}]} + } + } + }; + + assert.deepEqual(mpc.getAudioTrackPlaylists_(), [ + {uri: 'foo'}, + {uri: 'bar'}, + {uri: 'fizz'}, + {uri: 'bazz'} + ], 'returns all dash style en playlist'); + + const main = []; + const alt = []; + + Object.keys(master.mediaGroups.AUDIO.main).forEach(function(k) { + main.push(master.mediaGroups.AUDIO.main[k]); + }); + + Object.keys(master.mediaGroups.AUDIO.alt).forEach(function(k) { + alt.push(master.mediaGroups.AUDIO.alt[k]); + }); + + mpc.mediaTypes_.AUDIO.groups = { + main, + alt + }; + mpc.mediaTypes_.AUDIO.activeTrack = () => ({label: 'fr'}); + + assert.deepEqual(mpc.getAudioTrackPlaylists_(), [ + {uri: 'foo-fr'}, + {uri: 'bar-fr'}, + {uri: 'fizz-fr'}, + {uri: 'bazz-fr'} + ], 'returns all dash style fr playlists'); + + delete master.mediaGroups.AUDIO.main.fr.playlists; + master.mediaGroups.AUDIO.main.fr.uri = 'fizz-fr'; + + delete master.mediaGroups.AUDIO.alt.fr.playlists; + master.mediaGroups.AUDIO.alt.fr.uri = 'buzz-fr'; + + assert.deepEqual(mpc.getAudioTrackPlaylists_(), [ + {uri: 'fizz-fr', label: 'fr'}, + {uri: 'buzz-fr', label: 'fr'} + ], 'returns all fr hls style playlists'); + +}); + QUnit.test('throws error when given an empty URL', function(assert) { const options = { src: 'test', @@ -5886,3 +5955,4 @@ QUnit.test('should delay loading of new playlist if lastRequest was less than ha this.env.log.warn.callCount = 0; }); + diff --git a/test/media-groups.test.js b/test/media-groups.test.js index 910f41d29..799154091 100644 --- a/test/media-groups.test.js +++ b/test/media-groups.test.js @@ -9,7 +9,7 @@ import noop from '../src/util/noop'; import { parseManifest } from '../src/manifest.js'; import manifests from 'create-test-data!manifests'; -QUnit.module('MediaGroups', { +const sharedHooks = { beforeEach(assert) { this.env = useFakeEnvironment(assert); this.clock = this.env.clock; @@ -18,1208 +18,1427 @@ QUnit.module('MediaGroups', { afterEach(assert) { this.env.restore(); } -}); - -QUnit.test( - 'createMediaTypes creates skeleton object for all supported media groups', - function(assert) { - const noopToString = noop.toString(); - const result = MediaGroups.createMediaTypes(); - - assert.ok(result.AUDIO, 'created AUDIO media group object'); - assert.deepEqual( - result.AUDIO.groups, {}, - 'created empty object for AUDIO groups' - ); - assert.deepEqual( - result.AUDIO.tracks, {}, - 'created empty object for AUDIO tracks' - ); - assert.equal( - result.AUDIO.activePlaylistLoader, null, - 'AUDIO activePlaylistLoader is null' - ); - assert.equal( - result.AUDIO.activeGroup.toString(), noopToString, - 'created noop function for AUDIO activeGroup' - ); - assert.equal( - result.AUDIO.activeTrack.toString(), noopToString, - 'created noop function for AUDIO activeTrack' - ); - assert.equal( - result.AUDIO.onGroupChanged.toString(), noopToString, - 'created noop function for AUDIO onGroupChanged' - ); - assert.equal( - result.AUDIO.onTrackChanged.toString(), noopToString, - 'created noop function for AUDIO onTrackChanged' - ); - - assert.ok(result.SUBTITLES, 'created SUBTITLES media group object'); - assert.deepEqual( - result.SUBTITLES.groups, {}, - 'created empty object for SUBTITLES groups' - ); - assert.deepEqual( - result.SUBTITLES.tracks, {}, - 'created empty object for SUBTITLES tracks' - ); - assert.equal( - result.SUBTITLES.activePlaylistLoader, null, - 'SUBTITLES activePlaylistLoader is null' - ); - assert.equal( - result.SUBTITLES.activeGroup.toString(), noopToString, - 'created noop function for SUBTITLES activeGroup' - ); - assert.equal( - result.SUBTITLES.activeTrack.toString(), noopToString, - 'created noop function for SUBTITLES activeTrack' - ); - assert.equal( - result.SUBTITLES.onGroupChanged.toString(), noopToString, - 'created noop function for SUBTITLES onGroupChanged' - ); - assert.equal( - result.SUBTITLES.onTrackChanged.toString(), noopToString, - 'created noop function for SUBTITLES onTrackChanged' - ); - - assert.ok(result['CLOSED-CAPTIONS'], 'created CLOSED-CAPTIONS media group object'); - assert.deepEqual( - result['CLOSED-CAPTIONS'].groups, {}, - 'created empty object for CLOSED-CAPTIONS groups' - ); - assert.deepEqual( - result['CLOSED-CAPTIONS'].tracks, {}, - 'created empty object for CLOSED-CAPTIONS tracks' - ); - assert.equal( - result['CLOSED-CAPTIONS'].activePlaylistLoader, null, - 'CLOSED-CAPTIONS activePlaylistLoader is null' - ); - assert.equal( - result['CLOSED-CAPTIONS'].activeGroup.toString(), noopToString, - 'created noop function for CLOSED-CAPTIONS activeGroup' - ); - assert.equal( - result['CLOSED-CAPTIONS'].activeTrack.toString(), noopToString, - 'created noop function for CLOSED-CAPTIONS activeTrack' - ); - assert.equal( - result['CLOSED-CAPTIONS'].onGroupChanged.toString(), noopToString, - 'created noop function for CLOSED-CAPTIONS onGroupChanged' - ); - assert.equal( - result['CLOSED-CAPTIONS'].onTrackChanged.toString(), noopToString, - 'created noop function for CLOSED-CAPTIONS onTrackChanged' - ); - } -); +}; + +QUnit.module('MediaGroups', function() { + QUnit.module('general', sharedHooks); + + QUnit.test( + 'createMediaTypes creates skeleton object for all supported media groups', + function(assert) { + const noopToString = noop.toString(); + const result = MediaGroups.createMediaTypes(); + + assert.ok(result.AUDIO, 'created AUDIO media group object'); + assert.deepEqual( + result.AUDIO.groups, {}, + 'created empty object for AUDIO groups' + ); + assert.deepEqual( + result.AUDIO.tracks, {}, + 'created empty object for AUDIO tracks' + ); + assert.equal( + result.AUDIO.activePlaylistLoader, null, + 'AUDIO activePlaylistLoader is null' + ); + assert.equal( + result.AUDIO.activeGroup.toString(), noopToString, + 'created noop function for AUDIO activeGroup' + ); + assert.equal( + result.AUDIO.activeTrack.toString(), noopToString, + 'created noop function for AUDIO activeTrack' + ); + assert.equal( + result.AUDIO.onGroupChanged.toString(), noopToString, + 'created noop function for AUDIO onGroupChanged' + ); + assert.equal( + result.AUDIO.onTrackChanged.toString(), noopToString, + 'created noop function for AUDIO onTrackChanged' + ); + + assert.ok(result.SUBTITLES, 'created SUBTITLES media group object'); + assert.deepEqual( + result.SUBTITLES.groups, {}, + 'created empty object for SUBTITLES groups' + ); + assert.deepEqual( + result.SUBTITLES.tracks, {}, + 'created empty object for SUBTITLES tracks' + ); + assert.equal( + result.SUBTITLES.activePlaylistLoader, null, + 'SUBTITLES activePlaylistLoader is null' + ); + assert.equal( + result.SUBTITLES.activeGroup.toString(), noopToString, + 'created noop function for SUBTITLES activeGroup' + ); + assert.equal( + result.SUBTITLES.activeTrack.toString(), noopToString, + 'created noop function for SUBTITLES activeTrack' + ); + assert.equal( + result.SUBTITLES.onGroupChanged.toString(), noopToString, + 'created noop function for SUBTITLES onGroupChanged' + ); + assert.equal( + result.SUBTITLES.onTrackChanged.toString(), noopToString, + 'created noop function for SUBTITLES onTrackChanged' + ); + + assert.ok(result['CLOSED-CAPTIONS'], 'created CLOSED-CAPTIONS media group object'); + assert.deepEqual( + result['CLOSED-CAPTIONS'].groups, {}, + 'created empty object for CLOSED-CAPTIONS groups' + ); + assert.deepEqual( + result['CLOSED-CAPTIONS'].tracks, {}, + 'created empty object for CLOSED-CAPTIONS tracks' + ); + assert.equal( + result['CLOSED-CAPTIONS'].activePlaylistLoader, null, + 'CLOSED-CAPTIONS activePlaylistLoader is null' + ); + assert.equal( + result['CLOSED-CAPTIONS'].activeGroup.toString(), noopToString, + 'created noop function for CLOSED-CAPTIONS activeGroup' + ); + assert.equal( + result['CLOSED-CAPTIONS'].activeTrack.toString(), noopToString, + 'created noop function for CLOSED-CAPTIONS activeTrack' + ); + assert.equal( + result['CLOSED-CAPTIONS'].onGroupChanged.toString(), noopToString, + 'created noop function for CLOSED-CAPTIONS onGroupChanged' + ); + assert.equal( + result['CLOSED-CAPTIONS'].onTrackChanged.toString(), noopToString, + 'created noop function for CLOSED-CAPTIONS onTrackChanged' + ); + } + ); -QUnit.test( - 'stopLoaders pauses segment loader and playlist loader when available', - function(assert) { - let segmentLoaderAbortCalls = 0; - let segmentLoaderPauseCalls = 0; - let playlistLoaderPauseCalls = 0; + QUnit.test( + 'stopLoaders pauses segment loader and playlist loader when available', + function(assert) { + let segmentLoaderAbortCalls = 0; + let segmentLoaderPauseCalls = 0; + let playlistLoaderPauseCalls = 0; - const segmentLoader = { - abort: () => segmentLoaderAbortCalls++, - pause: () => segmentLoaderPauseCalls++ - }; - const playlistLoader = { - pause: () => playlistLoaderPauseCalls++ - }; - const mediaType = { activePlaylistLoader: null }; + const segmentLoader = { + abort: () => segmentLoaderAbortCalls++, + pause: () => segmentLoaderPauseCalls++ + }; + const playlistLoader = { + pause: () => playlistLoaderPauseCalls++ + }; + const mediaType = { activePlaylistLoader: null }; - MediaGroups.stopLoaders(segmentLoader, mediaType); + MediaGroups.stopLoaders(segmentLoader, mediaType); - assert.equal(segmentLoaderAbortCalls, 1, 'aborted segment loader'); - assert.equal(segmentLoaderPauseCalls, 1, 'paused segment loader'); - assert.equal(playlistLoaderPauseCalls, 0, 'no pause when no active playlist loader'); + assert.equal(segmentLoaderAbortCalls, 1, 'aborted segment loader'); + assert.equal(segmentLoaderPauseCalls, 1, 'paused segment loader'); + assert.equal(playlistLoaderPauseCalls, 0, 'no pause when no active playlist loader'); - mediaType.activePlaylistLoader = playlistLoader; + mediaType.activePlaylistLoader = playlistLoader; - MediaGroups.stopLoaders(segmentLoader, mediaType); + MediaGroups.stopLoaders(segmentLoader, mediaType); - assert.equal(segmentLoaderAbortCalls, 2, 'aborted segment loader'); - assert.equal(segmentLoaderPauseCalls, 2, 'paused segment loader'); - assert.equal(playlistLoaderPauseCalls, 1, 'pause active playlist loader'); - assert.equal( - mediaType.activePlaylistLoader, null, - 'clears active playlist loader for media group' - ); - } -); + assert.equal(segmentLoaderAbortCalls, 2, 'aborted segment loader'); + assert.equal(segmentLoaderPauseCalls, 2, 'paused segment loader'); + assert.equal(playlistLoaderPauseCalls, 1, 'pause active playlist loader'); + assert.equal( + mediaType.activePlaylistLoader, null, + 'clears active playlist loader for media group' + ); + } + ); -QUnit.test( - 'startLoaders starts playlist loader when appropriate', - function(assert) { - let playlistLoaderLoadCalls = 0; - const media = null; + QUnit.test( + 'startLoaders starts playlist loader when appropriate', + function(assert) { + let playlistLoaderLoadCalls = 0; + const media = null; - const playlistLoader = { - load: () => playlistLoaderLoadCalls++, - media: () => media - }; - const mediaType = { activePlaylistLoader: null }; + const playlistLoader = { + load: () => playlistLoaderLoadCalls++, + media: () => media + }; + const mediaType = { activePlaylistLoader: null }; - MediaGroups.startLoaders(playlistLoader, mediaType); + MediaGroups.startLoaders(playlistLoader, mediaType); - assert.equal(playlistLoaderLoadCalls, 1, 'called load on playlist loader'); - assert.strictEqual( - mediaType.activePlaylistLoader, playlistLoader, - 'set active playlist loader for media group' - ); - } -); + assert.equal(playlistLoaderLoadCalls, 1, 'called load on playlist loader'); + assert.strictEqual( + mediaType.activePlaylistLoader, playlistLoader, + 'set active playlist loader for media group' + ); + } + ); -QUnit.test('activeTrack returns the correct audio track', function(assert) { - const type = 'AUDIO'; - const settings = { mediaTypes: MediaGroups.createMediaTypes() }; - const tracks = settings.mediaTypes[type].tracks; - const activeTrack = MediaGroups.activeTrack[type](type, settings); + QUnit.test('activeTrack returns the correct audio track', function(assert) { + const type = 'AUDIO'; + const settings = { mediaTypes: MediaGroups.createMediaTypes() }; + const tracks = settings.mediaTypes[type].tracks; + const activeTrack = MediaGroups.activeTrack[type](type, settings); - assert.equal(activeTrack(), null, 'returns null when empty track list'); + assert.equal(activeTrack(), null, 'returns null when empty track list'); - tracks.track1 = { id: 'track1', enabled: false }; - tracks.track2 = { id: 'track2', enabled: false }; - tracks.track3 = { id: 'track3', enabled: false }; + tracks.track1 = { id: 'track1', enabled: false }; + tracks.track2 = { id: 'track2', enabled: false }; + tracks.track3 = { id: 'track3', enabled: false }; - assert.equal(activeTrack(), null, 'returns null when no active tracks'); + assert.equal(activeTrack(), null, 'returns null when no active tracks'); - tracks.track3.enabled = true; + tracks.track3.enabled = true; - assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); + assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); - tracks.track1.enabled = true; + tracks.track1.enabled = true; - // video.js treats the first enabled track in the track list as the active track - // so we want the same behavior here - assert.strictEqual(activeTrack(), tracks.track1, 'returns first active track'); + // video.js treats the first enabled track in the track list as the active track + // so we want the same behavior here + assert.strictEqual(activeTrack(), tracks.track1, 'returns first active track'); - tracks.track1.enabled = false; + tracks.track1.enabled = false; - assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); + assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); - tracks.track3.enabled = false; + tracks.track3.enabled = false; - assert.equal(activeTrack(), null, 'returns null when no active tracks'); -}); + assert.equal(activeTrack(), null, 'returns null when no active tracks'); + }); -QUnit.test('activeTrack returns the correct subtitle track', function(assert) { - const type = 'SUBTITLES'; - const settings = { mediaTypes: MediaGroups.createMediaTypes() }; - const tracks = settings.mediaTypes[type].tracks; - const activeTrack = MediaGroups.activeTrack[type](type, settings); + QUnit.test('activeTrack returns the correct subtitle track', function(assert) { + const type = 'SUBTITLES'; + const settings = { mediaTypes: MediaGroups.createMediaTypes() }; + const tracks = settings.mediaTypes[type].tracks; + const activeTrack = MediaGroups.activeTrack[type](type, settings); - assert.equal(activeTrack(), null, 'returns null when empty track list'); + assert.equal(activeTrack(), null, 'returns null when empty track list'); - tracks.track1 = { id: 'track1', mode: 'disabled' }; - tracks.track2 = { id: 'track2', mode: 'disabled' }; - tracks.track3 = { id: 'track3', mode: 'disabled' }; + tracks.track1 = { id: 'track1', mode: 'disabled' }; + tracks.track2 = { id: 'track2', mode: 'disabled' }; + tracks.track3 = { id: 'track3', mode: 'disabled' }; - assert.equal(activeTrack(), null, 'returns null when no active tracks'); + assert.equal(activeTrack(), null, 'returns null when no active tracks'); - tracks.track3.mode = 'showing'; + tracks.track3.mode = 'showing'; - assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); + assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); - tracks.track1.mode = 'showing'; + tracks.track1.mode = 'showing'; - // video.js treats the first enabled track in the track list as the active track - // so we want the same behavior here - assert.strictEqual(activeTrack(), tracks.track1, 'returns first active track'); + // video.js treats the first enabled track in the track list as the active track + // so we want the same behavior here + assert.strictEqual(activeTrack(), tracks.track1, 'returns first active track'); - tracks.track1.mode = 'disabled'; + tracks.track1.mode = 'disabled'; - assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); + assert.strictEqual(activeTrack(), tracks.track3, 'returns active track'); - tracks.track2.mode = 'hidden'; - tracks.track3.mode = 'disabled'; + tracks.track2.mode = 'hidden'; + tracks.track3.mode = 'disabled'; - assert.equal(activeTrack(), tracks.track2, 'returns hidden active track'); + assert.equal(activeTrack(), tracks.track2, 'returns hidden active track'); - tracks.track2.mode = 'disabled'; + tracks.track2.mode = 'disabled'; - assert.equal(activeTrack(), null, 'returns null when no active tracks'); -}); + assert.equal(activeTrack(), null, 'returns null when no active tracks'); + }); -QUnit.test('activeGroup returns the correct audio group', function(assert) { - const type = 'AUDIO'; - let media = null; - const settings = { - mediaTypes: MediaGroups.createMediaTypes(), - masterPlaylistLoader: { - media: () => media - } - }; - const groups = settings.mediaTypes[type].groups; - const tracks = settings.mediaTypes[type].tracks; - const activeTrack = MediaGroups.activeTrack[type](type, settings); - const activeGroup = MediaGroups.activeGroup(type, settings); + ['AUDIO', 'SUBTITLES'].forEach(function(groupType) { + QUnit.module(`${groupType} activeGroup `, { + beforeEach(assert) { + sharedHooks.beforeEach.call(this, assert); - assert.equal(activeGroup(), null, 'returns null when no media in masterPlaylistLoader'); + this.media = null; - media = { attributes: { } }; - groups.main = [{ id: 'en' }, { id: 'fr' }]; + const settings = { + mediaTypes: MediaGroups.createMediaTypes(), + masterPlaylistLoader: { + media: () => this.media + } + }; - assert.strictEqual( - activeGroup(), groups.main, - 'defaults to main audio group when media does not specify audio group' - ); + this.groups = settings.mediaTypes[groupType].groups; + this.tracks = settings.mediaTypes[groupType].tracks; + this.activeTrack = MediaGroups.activeTrack[groupType](groupType, settings); + this.activeGroup = MediaGroups.activeGroup(groupType, settings); + }, + afterEach(assert) { + sharedHooks.afterEach.call(this, assert); + } + }); - groups.audio = [{ id: 'en'}, { id: 'fr' }]; - media.attributes.AUDIO = 'audio'; + QUnit.test('activeGroup without media', function(assert) { + assert.equal(this.activeGroup(), null, 'no media or groups'); - assert.strictEqual( - activeGroup(), groups.audio, - 'returns list of variants in active audio group' - ); + this.groups.foo = [{ id: 'en' }, { id: 'fr' }]; + this.groups.bar = [{ id: 'en' }, { id: 'fr' }]; - tracks.en = { id: 'en', enabled: false }; - tracks.fr = { id: 'fr', enabled: false }; + assert.equal(this.activeGroup(), null, 'no media, with groups'); + }); - assert.equal( - activeGroup(activeTrack()), null, - 'returns null when an active track is specified, but there is no active track' - ); + QUnit.test('activeGroup with media but no group', function(assert) { + this.media = {attributes: {}}; + this.groups.main = [{ id: 'en' }, { id: 'fr' }]; - tracks.fr.enabled = true; + assert.equal(this.activeGroup(), this.groups.main, 'main when there is a main'); - assert.strictEqual( - activeGroup(activeTrack()), groups.audio[1], - 'returned the active group corresponding to the active track' - ); -}); + delete this.groups.main; + this.groups.foo = [{id: 'en'}, {id: 'fr'}]; -QUnit.test('activeGroup returns the correct subtitle group', function(assert) { - const type = 'SUBTITLES'; - let media = null; - const settings = { - mediaTypes: MediaGroups.createMediaTypes(), - masterPlaylistLoader: { - media: () => media - } - }; - const groups = settings.mediaTypes[type].groups; - const tracks = settings.mediaTypes[type].tracks; - const activeTrack = MediaGroups.activeTrack[type](type, settings); - const activeGroup = MediaGroups.activeGroup(type, settings); + assert.equal(this.activeGroup(), this.groups.foo, 'the only group if there is only one'); - assert.equal(activeGroup(), null, 'returns null when no media in masterPlaylistLoader'); + this.groups.foo = [{id: 'en'}, {id: 'fr'}]; + this.groups.bar = [{id: 'en'}, {id: 'fr'}]; - media = { attributes: { } }; + assert.equal(this.activeGroup(), null, 'too many groups to select one'); + }); - // there is no default `main` group for subtitles like there is for audio - assert.notOk(activeGroup(), 'returns null when media does not specify subtitle group'); + QUnit.test('activeGroup with media and group', function(assert) { + this.media = {attributes: {AUDIO: 'foo'}}; - groups.subs = [{ id: 'en'}, { id: 'fr' }]; - media.attributes.SUBTITLES = 'subs'; + this.groups.main = [{ id: 'en' }, { id: 'fr' }]; + this.groups.foo = [{ id: 'en' }, { id: 'fr' }]; - assert.strictEqual( - activeGroup(), groups.subs, - 'returns list of variants in active subtitle group' - ); + assert.deepEqual(this.activeGroup(), this.groups.foo, 'selected attribute group'); + }); - tracks.en = { id: 'en', mode: 'disabled' }; - tracks.fr = { id: 'fr', mode: 'disabled' }; + QUnit.test('activeGroup passed a track', function(assert) { + this.media = {attributes: {AUDIO: 'foo'}}; - assert.equal( - activeGroup(activeTrack()), null, - 'returns null when an active track is specified, but there is no active track' - ); + this.groups.main = [{ id: 'en' }, { id: 'fr' }]; + this.groups.foo = [{ id: 'en' }, { id: 'fr' }]; - tracks.fr.mode = 'showing'; + assert.equal(this.activeGroup(null), null, 'no group when passed null track'); + assert.deepEqual(this.activeGroup({id: 'en'}), this.groups.foo[0], 'returns track when passed a valid track'); + assert.equal(this.activeGroup({id: 'baz'}), null, 'no group with invalid track'); + }); - assert.strictEqual( - activeGroup(activeTrack()), groups.subs[1], - 'returned the active group corresponding to the active track' - ); -}); + QUnit.module(`${groupType} getActiveGroup `, { + beforeEach(assert) { + sharedHooks.beforeEach.call(this, assert); -QUnit.test('onGroupChanging aborts and pauses segment loaders', function(assert) { - const calls = { - abort: 0, - pause: 0 - }; - const segmentLoader = { - abort: () => calls.abort++, - pause: () => calls.pause++ - }; + const settings = {mediaTypes: {}}; - const settings = { - segmentLoaders: { - AUDIO: segmentLoader - } - }; - const type = 'AUDIO'; + this.groupType = settings.mediaTypes[groupType] = { + activeGroup: () => {}, + activeTrack: () => {} + }; - const onGroupChanging = MediaGroups.onGroupChanging(type, settings); + this.getActiveGroup = MediaGroups.getActiveGroup(groupType, settings); + }, + afterEach(assert) { + sharedHooks.afterEach.call(this, assert); + } + }); - assert.deepEqual(calls, {abort: 0, pause: 0}, 'no calls yet'); + QUnit.test('works as expected', function(assert) { + assert.equal(this.getActiveGroup(), null, 'no active group, without active track'); - onGroupChanging(); + this.groupType.activeTrack = () => ({id: 'en'}); + this.groupType.activeGroup = () => ({foo: true}); - assert.deepEqual(calls, {abort: 1, pause: 1}, 'one abort one pause'); -}); + assert.deepEqual(this.getActiveGroup(), {foo: true}, 'returns activeGroup with active track'); + }); + }); -QUnit.test( - 'onGroupChanged updates active playlist loader and resyncs segment loader', - function(assert) { - let mainSegmentLoaderResetCalls = 0; - let segmentLoaderResyncCalls = 0; - let segmentLoaderPauseCalls = 0; + QUnit.module('onGroupChanging', sharedHooks); - const type = 'AUDIO'; - const media = { attributes: { AUDIO: 'main' } }; - const mainSegmentLoader = { resetEverything: () => mainSegmentLoaderResetCalls++ }; - const segmentLoader = { - abort() {}, - pause: () => segmentLoaderPauseCalls++, - load() {}, - playlist() {}, - resyncLoader: () => segmentLoaderResyncCalls++ + QUnit.test('onGroupChanging aborts and pauses segment loaders', function(assert) { + const calls = { + abort: 0, + pause: 0 }; - const mockPlaylistLoader = () => { - return { - media: () => media, - load() {}, - pause() {} - }; + const segmentLoader = { + abort: () => calls.abort++, + pause: () => calls.pause++ }; - const masterPlaylistLoader = mockPlaylistLoader(); + const settings = { segmentLoaders: { - AUDIO: segmentLoader, - main: mainSegmentLoader + AUDIO: segmentLoader }, - mediaTypes: MediaGroups.createMediaTypes(), - masterPlaylistLoader + mediaTypes: { + AUDIO: {} + } }; - const mediaType = settings.mediaTypes[type]; - const groups = mediaType.groups; - const tracks = mediaType.tracks; + const type = 'AUDIO'; - groups.main = [ - { id: 'en', playlistLoader: null }, - { id: 'fr', playlistLoader: mockPlaylistLoader() }, - { id: 'es', playlistLoader: mockPlaylistLoader() } - ]; - tracks.en = { id: 'en', enabled: false }; - tracks.fr = { id: 'fr', enabled: false }; - tracks.es = { id: 'es', enabled: false }; - mediaType.activeTrack = MediaGroups.activeTrack[type](type, settings); - mediaType.activeGroup = MediaGroups.activeGroup(type, settings); + const onGroupChanging = MediaGroups.onGroupChanging(type, settings); - const onGroupChanged = MediaGroups.onGroupChanged(type, settings); + assert.deepEqual(calls, {abort: 0, pause: 0}, 'no calls yet'); - onGroupChanged(); + onGroupChanging(); - assert.equal(segmentLoaderPauseCalls, 1, 'loaders paused on group change'); - assert.equal(mainSegmentLoaderResetCalls, 0, 'no reset when no active group'); - assert.equal(segmentLoaderResyncCalls, 0, 'no resync when no active group'); + assert.deepEqual(calls, {abort: 1, pause: 1}, 'one abort one pause'); + }); - tracks.en.enabled = true; + QUnit.module('onGroupChanged', sharedHooks); - onGroupChanged(); + QUnit.test( + 'onGroupChanged updates active playlist loader and resyncs segment loader', + function(assert) { + let mainSegmentLoaderResetCalls = 0; + let segmentLoaderResyncCalls = 0; + let segmentLoaderPauseCalls = 0; - assert.equal(segmentLoaderPauseCalls, 2, 'loaders paused on group change'); - assert.equal( - mainSegmentLoaderResetCalls, 0, - 'no reset changing from no active playlist loader to group with no playlist loader' - ); - assert.equal( - segmentLoaderResyncCalls, 0, - 'no resync changing to group with no playlist loader' - ); + const type = 'AUDIO'; + const media = { attributes: { AUDIO: 'main' } }; + const mainSegmentLoader = { resetEverything: () => mainSegmentLoaderResetCalls++ }; + const segmentLoader = { + abort() {}, + pause: () => segmentLoaderPauseCalls++, + load() {}, + playlist() {}, + resyncLoader: () => segmentLoaderResyncCalls++ + }; + const mockPlaylistLoader = () => { + return { + media: () => media, + load() {}, + pause() {} + }; + }; + const masterPlaylistLoader = mockPlaylistLoader(); + const settings = { + segmentLoaders: { + AUDIO: segmentLoader, + main: mainSegmentLoader + }, + mediaTypes: MediaGroups.createMediaTypes(), + masterPlaylistLoader + }; + const mediaType = settings.mediaTypes[type]; + const groups = mediaType.groups; + const tracks = mediaType.tracks; + + groups.main = [ + { id: 'en', playlistLoader: null }, + { id: 'fr', playlistLoader: mockPlaylistLoader() }, + { id: 'es', playlistLoader: mockPlaylistLoader() } + ]; + tracks.en = { id: 'en', enabled: false }; + tracks.fr = { id: 'fr', enabled: false }; + tracks.es = { id: 'es', enabled: false }; + mediaType.activeTrack = MediaGroups.activeTrack[type](type, settings); + mediaType.activeGroup = MediaGroups.activeGroup(type, settings); + mediaType.getActiveGroup = MediaGroups.getActiveGroup(type, settings); + + const onGroupChanged = MediaGroups.onGroupChanged(type, settings); + + onGroupChanged(); + + assert.equal(segmentLoaderPauseCalls, 1, 'paused loader with no active group'); + assert.equal(mainSegmentLoaderResetCalls, 0, 'no reset when no active group'); + assert.equal(segmentLoaderResyncCalls, 0, 'no resync when no active group'); + + tracks.en.enabled = true; + + onGroupChanged(); + + assert.equal(segmentLoaderPauseCalls, 2, 'loaders paused on group change'); + assert.equal( + mainSegmentLoaderResetCalls, 0, + 'no reset changing from no active playlist loader to group with no playlist loader' + ); + assert.equal( + segmentLoaderResyncCalls, 0, + 'no resync changing to group with no playlist loader' + ); + + mediaType.lastGroup_ = null; + mediaType.activePlaylistLoader = groups.main[1].playlistLoader; + onGroupChanged(); + + assert.equal(segmentLoaderPauseCalls, 3, 'loaders paused on group change'); + assert.equal( + mainSegmentLoaderResetCalls, 1, + 'reset changing from active playlist loader to group with no playlist loader' + ); + assert.equal( + segmentLoaderResyncCalls, 0, + 'no resync changing to group with no playlist loader' + ); + + mediaType.lastGroup_ = null; + tracks.en.enabled = false; + tracks.fr.enabled = true; + mediaType.activePlaylistLoader = groups.main[2].playlistLoader; + onGroupChanged(); + + assert.equal(segmentLoaderPauseCalls, 4, 'loaders paused on group change'); + assert.equal( + mainSegmentLoaderResetCalls, 1, + 'no reset changing to group with playlist loader' + ); + assert.equal( + segmentLoaderResyncCalls, 1, + 'resync changing to group with playlist loader' + ); + assert.strictEqual( + mediaType.activePlaylistLoader, groups.main[1].playlistLoader, + 'sets the correct active playlist loader' + ); + + mediaType.lastGroup_ = null; + groups.main[1].isMasterPlaylist = true; + + onGroupChanged(); + + assert.equal(segmentLoaderPauseCalls, 5, 'loaders paused on group change'); + assert.equal(mainSegmentLoaderResetCalls, 1, 'main segment loader not reset'); + + onGroupChanged(); + + assert.equal(segmentLoaderPauseCalls, 5, 'loader not paused without group change'); + assert.equal(mainSegmentLoaderResetCalls, 1, 'main segment loader not reset without group change'); - mediaType.activePlaylistLoader = groups.main[1].playlistLoader; + } + ); - onGroupChanged(); + QUnit.module('onTrackChanged', sharedHooks); + + QUnit.test( + 'onTrackChanged updates active playlist loader and resets segment loader', + function(assert) { + let mainSegmentLoaderResetCalls = 0; + const mainSegmentLoaderSetAudioCalls = []; + let segmentLoaderResetCalls = 0; + const segmentLoaderSetAudioCalls = []; + let segmentLoaderPauseCalls = 0; + let segmentLoaderTrack; + + const type = 'AUDIO'; + const media = { attributes: { AUDIO: 'main' } }; + const mainSegmentLoader = { + setAudio: (enable) => mainSegmentLoaderSetAudioCalls.push(enable), + resetEverything: () => mainSegmentLoaderResetCalls++ + }; + const segmentLoader = { + abort() {}, + pause: () => segmentLoaderPauseCalls++, + playlist() {}, + setAudio: (enable) => segmentLoaderSetAudioCalls.push(enable), + resetEverything: () => segmentLoaderResetCalls++ + }; + const mockPlaylistLoader = () => { + return { + media: () => media, + load() {}, + pause() {} + }; + }; + const masterPlaylistLoader = mockPlaylistLoader(); + const settings = { + segmentLoaders: { + AUDIO: segmentLoader, + main: mainSegmentLoader + }, + mediaTypes: MediaGroups.createMediaTypes(), + masterPlaylistLoader + }; + const mediaType = settings.mediaTypes[type]; + const groups = mediaType.groups; + const tracks = mediaType.tracks; + + groups.main = [ + { id: 'en', playlistLoader: null }, + { id: 'fr', playlistLoader: mockPlaylistLoader() }, + { id: 'es', playlistLoader: mockPlaylistLoader() } + ]; + tracks.en = { id: 'en', enabled: false }; + tracks.fr = { id: 'fr', enabled: false }; + tracks.es = { id: 'es', enabled: false }; + mediaType.activeTrack = MediaGroups.activeTrack[type](type, settings); + mediaType.activeGroup = MediaGroups.activeGroup(type, settings); + mediaType.getActiveGroup = MediaGroups.getActiveGroup(type, settings); + + const onTrackChanged = MediaGroups.onTrackChanged(type, settings); + + onTrackChanged(); + + assert.equal(segmentLoaderPauseCalls, 1, 'loaders paused on track change'); + assert.equal(mainSegmentLoaderResetCalls, 0, 'no main reset when no active group'); + assert.equal(mainSegmentLoaderSetAudioCalls.length, 0, 'no main setAudio when no active group'); + assert.equal(segmentLoaderResetCalls, 0, 'no reset when no active group'); + assert.equal(segmentLoaderSetAudioCalls.length, 0, 'no setAudio when no active group'); + + tracks.en.enabled = true; + + onTrackChanged(); + + assert.equal(segmentLoaderPauseCalls, 2, 'loaders paused on track change'); + assert.equal( + mainSegmentLoaderResetCalls, 1, + 'main reset when changing to group with no playlist loader' + ); + assert.equal( + mainSegmentLoaderSetAudioCalls.length, 1, + 'main audio set when changing to group with no playlist loader' + ); + assert.ok(mainSegmentLoaderSetAudioCalls[0], 'main audio set to true'); + assert.equal( + segmentLoaderResetCalls, 0, + 'no reset changing to group with no playlist loader' + ); + assert.equal( + segmentLoaderSetAudioCalls.length, 0, + 'no audio set when changing to group with no playlist loader' + ); + + tracks.en.enabled = false; + tracks.fr.enabled = true; + mediaType.activePlaylistLoader = groups.main[1].playlistLoader; + + onTrackChanged(); + + assert.equal(segmentLoaderPauseCalls, 3, 'loaders paused on track change'); + assert.equal( + mainSegmentLoaderResetCalls, 1, + 'no main reset changing to group with playlist loader' + ); + assert.equal( + mainSegmentLoaderSetAudioCalls.length, 2, + 'main audio set when changing to group with playlist loader' + ); + assert.notOk(mainSegmentLoaderSetAudioCalls[1], 'main audio set to true'); + assert.equal( + segmentLoaderResetCalls, 0, + 'no reset when active group hasn\'t changed' + ); + assert.equal(segmentLoaderSetAudioCalls.length, 1, 'set audio on track change'); + assert.ok(segmentLoaderSetAudioCalls[0], 'enabled audio on track change'); + assert.strictEqual( + mediaType.activePlaylistLoader, groups.main[1].playlistLoader, + 'sets the correct active playlist loader' + ); + + tracks.fr.enabled = false; + tracks.es.enabled = true; + + onTrackChanged(); + + assert.equal(segmentLoaderPauseCalls, 4, 'loaders paused on track change'); + assert.equal( + mainSegmentLoaderResetCalls, 1, + 'no main reset changing to group with playlist loader' + ); + assert.equal( + mainSegmentLoaderSetAudioCalls.length, 3, + 'main audio set when changing to group with playlist loader' + ); + assert.notOk( + mainSegmentLoaderSetAudioCalls[2], + 'main audio set to false when changing to group with playlist loader' + ); + assert.equal(segmentLoaderSetAudioCalls.length, 2, 'audio set on track change'); + assert.ok(segmentLoaderSetAudioCalls[1], 'audio enabled on track change'); + assert.equal(segmentLoaderResetCalls, 1, 'reset on track change'); + assert.strictEqual( + mediaType.activePlaylistLoader, groups.main[2].playlistLoader, + 'sets the correct active playlist loader' + ); + + // setting the track on the segment loader only applies to the SUBTITLES case. + // even though this test is testing type AUDIO, aside from this difference of setting + // the track, the functionality between the types is the same. + segmentLoader.track = (track) => { + segmentLoaderTrack = track; + }; - assert.equal(segmentLoaderPauseCalls, 3, 'loaders paused on group change'); - assert.equal( - mainSegmentLoaderResetCalls, 1, - 'reset changing from active playlist loader to group with no playlist loader' - ); - assert.equal( - segmentLoaderResyncCalls, 0, - 'no resync changing to group with no playlist loader' - ); + tracks.fr.enabled = true; + tracks.es.enabled = false; + + onTrackChanged(); + + assert.equal(segmentLoaderPauseCalls, 5, 'loaders paused on track change'); + assert.equal( + mainSegmentLoaderResetCalls, 1, + 'no main reset changing to group with playlist loader' + ); + assert.equal( + mainSegmentLoaderSetAudioCalls.length, 4, + 'main audio set when changing to group with playlist loader' + ); + assert.notOk( + mainSegmentLoaderSetAudioCalls[3], + 'main audio set to false when changing to group with playlist loader' + ); + assert.equal(segmentLoaderSetAudioCalls.length, 3, 'audio set on track change'); + assert.ok(segmentLoaderSetAudioCalls[2], 'audio enabled on track change'); + assert.equal( + segmentLoaderResetCalls, 2, + 'reset on track change' + ); + assert.strictEqual( + mediaType.activePlaylistLoader, groups.main[1].playlistLoader, + 'sets the correct active playlist loader' + ); + assert.strictEqual( + segmentLoaderTrack, tracks.fr, + 'set the correct track on the segment loader' + ); + } + ); - tracks.en.enabled = false; - tracks.fr.enabled = true; - mediaType.activePlaylistLoader = groups.main[2].playlistLoader; + const createMocker = ({calls = [], args = []} = {}) => { + const obj = {calls: {}, args: {}}; - onGroupChanged(); + calls.forEach(function(key) { + obj.calls[key] = 0; + obj[key] = () => { + obj.calls[key]++; + }; + }); - assert.equal(segmentLoaderPauseCalls, 4, 'loaders paused on group change'); - assert.equal( - mainSegmentLoaderResetCalls, 1, - 'no reset changing to group with playlist loader' - ); - assert.equal( - segmentLoaderResyncCalls, 1, - 'resync changing to group with playlist loader' - ); - assert.strictEqual( - mediaType.activePlaylistLoader, groups.main[1].playlistLoader, - 'sets the correct active playlist loader' - ); - } -); - -QUnit.test( - 'onTrackChanged updates active playlist loader and resets segment loader', - function(assert) { - let mainSegmentLoaderResetCalls = 0; - const mainSegmentLoaderSetAudioCalls = []; - let segmentLoaderResetCalls = 0; - const segmentLoaderSetAudioCalls = []; - let segmentLoaderPauseCalls = 0; - let segmentLoaderTrack; + args.forEach(function(key) { + obj.args[key] = []; + obj[key] = (v) => { + obj.args[key].push(v); + }; + }); + + return obj; + }; + const mockSegmentLoader = () => createMocker({ + calls: ['abort', 'pause', 'resetEverything'], + args: ['setAudio', 'track'] + }); + const mockPlaylistLoader = () => createMocker({ + calls: ['pause', 'load'], + args: ['fastQualityChange_'] + }); + + const mocksAreZero = (mocks, assert) => { + Object.keys(mocks).forEach(function(name) { + const mock = mocks[name]; + + Object.keys(mock.calls).forEach(function(key) { + assert.equal(mock.calls[key], 0, `${name} ${key} not called`); + }); + + Object.keys(mock.args).forEach(function(key) { + assert.equal(mock.args[key].length, 0, `${name} ${key} not called`); + }); + }); + }; + + QUnit.test('onTrackChanged with isMasterPlaylist', function(assert) { + this.media = {id: 'en', attributes: {AUDIO: 'main'}}; + this.nextMedia = {id: 'fr', attributes: {AUDIO: 'main'}}; + + const audioSegmentLoader = mockSegmentLoader(); + const mainSegmentLoader = mockSegmentLoader(); + const masterPlaylistLoader = Object.assign(mockPlaylistLoader(), { + media: () => this.media + }); + const masterPlaylistController_ = Object.assign(mockPlaylistLoader(), { + media: () => this.media, + selectPlaylist: () => this.nextMedia + }); + const mocks = {audioSegmentLoader, mainSegmentLoader, masterPlaylistController_, masterPlaylistLoader}; const type = 'AUDIO'; - const media = { attributes: { AUDIO: 'main' } }; - const mainSegmentLoader = { - setAudio: (enable) => mainSegmentLoaderSetAudioCalls.push(enable), - resetEverything: () => mainSegmentLoaderResetCalls++ - }; - const segmentLoader = { - abort() {}, - pause: () => segmentLoaderPauseCalls++, - playlist() {}, - setAudio: (enable) => segmentLoaderSetAudioCalls.push(enable), - resetEverything: () => segmentLoaderResetCalls++ - }; - const mockPlaylistLoader = () => { - return { - media: () => media, - load() {}, - pause() {} - }; - }; - const masterPlaylistLoader = mockPlaylistLoader(); const settings = { segmentLoaders: { - AUDIO: segmentLoader, + AUDIO: audioSegmentLoader, main: mainSegmentLoader }, mediaTypes: MediaGroups.createMediaTypes(), - masterPlaylistLoader + masterPlaylistLoader, + vhs: { + masterPlaylistController_ + } }; const mediaType = settings.mediaTypes[type]; const groups = mediaType.groups; const tracks = mediaType.tracks; groups.main = [ - { id: 'en', playlistLoader: null }, - { id: 'fr', playlistLoader: mockPlaylistLoader() }, - { id: 'es', playlistLoader: mockPlaylistLoader() } + { id: 'en', playlistLoader: null, isMasterPlaylist: true }, + { id: 'fr', playlistLoader: null, isMasterPlaylist: true }, + { id: 'es', playlistLoader: null, isMasterPlaylist: true } ]; - tracks.en = { id: 'en', enabled: false }; + tracks.en = { id: 'en', enabled: true }; tracks.fr = { id: 'fr', enabled: false }; tracks.es = { id: 'es', enabled: false }; mediaType.activeTrack = MediaGroups.activeTrack[type](type, settings); mediaType.activeGroup = MediaGroups.activeGroup(type, settings); + mediaType.getActiveGroup = MediaGroups.getActiveGroup(type, settings); const onTrackChanged = MediaGroups.onTrackChanged(type, settings); + // intial track setup does nothing. onTrackChanged(); - assert.equal(segmentLoaderPauseCalls, 1, 'loaders paused on track change'); - assert.equal(mainSegmentLoaderResetCalls, 0, 'no main reset when no active group'); - assert.equal(mainSegmentLoaderSetAudioCalls.length, 0, 'no main setAudio when no active group'); - assert.equal(segmentLoaderResetCalls, 0, 'no reset when no active group'); - assert.equal(segmentLoaderSetAudioCalls.length, 0, 'no setAudio when no active group'); + assert.equal(audioSegmentLoader.calls.pause, 1, 'audioSegmentLoader pause called'); + assert.equal(audioSegmentLoader.calls.abort, 1, 'audioSegmentLoader abort called'); - tracks.en.enabled = true; + audioSegmentLoader.calls.pause = 0; + audioSegmentLoader.calls.abort = 0; - onTrackChanged(); - - assert.equal(segmentLoaderPauseCalls, 2, 'loaders paused on track change'); - assert.equal( - mainSegmentLoaderResetCalls, 1, - 'main reset when changing to group with no playlist loader' - ); - assert.equal( - mainSegmentLoaderSetAudioCalls.length, 1, - 'main audio set when changing to group with no playlist loader' - ); - assert.ok(mainSegmentLoaderSetAudioCalls[0], 'main audio set to true'); - assert.equal( - segmentLoaderResetCalls, 0, - 'no reset changing to group with no playlist loader' - ); - assert.equal( - segmentLoaderSetAudioCalls.length, 0, - 'no audio set when changing to group with no playlist loader' - ); + // verify that all other mocks are zero + mocksAreZero(mocks, assert); tracks.en.enabled = false; tracks.fr.enabled = true; - mediaType.activePlaylistLoader = groups.main[1].playlistLoader; onTrackChanged(); - assert.equal(segmentLoaderPauseCalls, 3, 'loaders paused on track change'); - assert.equal( - mainSegmentLoaderResetCalls, 1, - 'no main reset changing to group with playlist loader' - ); - assert.equal( - mainSegmentLoaderSetAudioCalls.length, 2, - 'main audio set when changing to group with playlist loader' - ); - assert.notOk(mainSegmentLoaderSetAudioCalls[1], 'main audio set to true'); - assert.equal( - segmentLoaderResetCalls, 0, - 'no reset when active group hasn\'t changed' - ); - assert.equal(segmentLoaderSetAudioCalls.length, 1, 'set audio on track change'); - assert.ok(segmentLoaderSetAudioCalls[0], 'enabled audio on track change'); - assert.strictEqual( - mediaType.activePlaylistLoader, groups.main[1].playlistLoader, - 'sets the correct active playlist loader' + assert.equal(audioSegmentLoader.calls.pause, 1, 'audioSegmentLoader pause called on track change'); + assert.equal(audioSegmentLoader.calls.abort, 1, 'audioSegmentLoader abort called on track change'); + assert.equal(mainSegmentLoader.calls.resetEverything, 1, 'mainSegmentLoader resetEverything called on track change'); + assert.equal(masterPlaylistLoader.calls.pause, 1, 'masterPlaylistLoader pause called on track change'); + assert.deepEqual( + masterPlaylistController_.args.fastQualityChange_, + [this.nextMedia], + 'fastQualityChange_ called on track change' ); - mediaType.activePlaylistLoader = groups.main[2].playlistLoader; + audioSegmentLoader.calls.pause = 0; + audioSegmentLoader.calls.abort = 0; + mainSegmentLoader.calls.resetEverything = 0; + masterPlaylistLoader.calls.pause = 0; + masterPlaylistController_.args.fastQualityChange_.length = 0; + mocksAreZero(mocks, assert); + + // mock track change without media change (via selectPlaylist) + this.media = this.nextMedia; + settings.mediaTypes.AUDIO.lastTrack_ = {id: 'en'}; onTrackChanged(); - assert.equal(segmentLoaderPauseCalls, 4, 'loaders paused on track change'); - assert.equal( - mainSegmentLoaderResetCalls, 1, - 'no main reset changing to group with playlist loader' - ); - assert.equal( - mainSegmentLoaderSetAudioCalls.length, 3, - 'main audio set when changing to group with playlist loader' - ); - assert.notOk( - mainSegmentLoaderSetAudioCalls[2], - 'main audio set to false when changing to group with playlist loader' - ); - assert.equal(segmentLoaderSetAudioCalls.length, 2, 'audio set on track change'); - assert.ok(segmentLoaderSetAudioCalls[1], 'audio enabled on track change'); - assert.equal(segmentLoaderResetCalls, 1, 'reset on track change'); - assert.strictEqual( - mediaType.activePlaylistLoader, groups.main[1].playlistLoader, - 'sets the correct active playlist loader' - ); + assert.equal(audioSegmentLoader.calls.pause, 1, 'audioSegmentLoader pause called'); + assert.equal(audioSegmentLoader.calls.abort, 1, 'audioSegmentLoader abort called'); - // setting the track on the segment loader only applies to the SUBTITLES case. - // even though this test is testing type AUDIO, aside from this difference of setting - // the track, the functionality between the types is the same. - segmentLoader.track = (track) => { - segmentLoaderTrack = track; - }; - mediaType.activePlaylistLoader = groups.main[2].playlistLoader; + audioSegmentLoader.calls.pause = 0; + audioSegmentLoader.calls.abort = 0; + + mocksAreZero(mocks, assert); + + tracks.en.enabled = true; + tracks.fr.enabled = false; + this.nextMedia = {id: 'en'}; onTrackChanged(); - assert.equal(segmentLoaderPauseCalls, 5, 'loaders paused on track change'); - assert.equal( - mainSegmentLoaderResetCalls, 1, - 'no main reset changing to group with playlist loader' - ); - assert.equal( - mainSegmentLoaderSetAudioCalls.length, 4, - 'main audio set when changing to group with playlist loader' - ); - assert.notOk( - mainSegmentLoaderSetAudioCalls[3], - 'main audio set to false when changing to group with playlist loader' - ); - assert.equal(segmentLoaderSetAudioCalls.length, 3, 'audio set on track change'); - assert.ok(segmentLoaderSetAudioCalls[2], 'audio enabled on track change'); - assert.equal( - segmentLoaderResetCalls, 2, - 'reset on track change' - ); - assert.strictEqual( - mediaType.activePlaylistLoader, groups.main[1].playlistLoader, - 'sets the correct active playlist loader' - ); - assert.strictEqual( - segmentLoaderTrack, tracks.fr, - 'set the correct track on the segment loader' + assert.equal(audioSegmentLoader.calls.pause, 1, 'audioSegmentLoader pause called on track change'); + assert.equal(audioSegmentLoader.calls.abort, 1, 'audioSegmentLoader abort called on track change'); + assert.equal(mainSegmentLoader.calls.resetEverything, 1, 'mainSegmentLoader resetEverything called on track change'); + assert.equal(masterPlaylistLoader.calls.pause, 1, 'masterPlaylistLoader pause called on track change'); + assert.deepEqual( + masterPlaylistController_.args.fastQualityChange_, + [this.nextMedia], + 'fastQualityChange_ called on track change' ); - } -); -QUnit.test( - 'switches to default audio track when an error is encountered', - function(assert) { - let blacklistCurrentPlaylistCalls = 0; - let onTrackChangedCalls = 0; + audioSegmentLoader.calls.pause = 0; + audioSegmentLoader.calls.abort = 0; + mainSegmentLoader.calls.resetEverything = 0; + masterPlaylistLoader.calls.pause = 0; + masterPlaylistController_.args.fastQualityChange_.length = 0; - const type = 'AUDIO'; + mocksAreZero(mocks, assert); + + // no changes as track is the same. + onTrackChanged(); + mocksAreZero(mocks, assert); + + }); + + QUnit.test( + 'switches to default audio track when an error is encountered', + function(assert) { + let blacklistCurrentPlaylistCalls = 0; + let onTrackChangedCalls = 0; + + const type = 'AUDIO'; + const segmentLoader = { abort() {}, pause() {} }; + const masterPlaylistLoader = { + media() { + return { attributes: { AUDIO: 'main' } }; + } + }; + const settings = { + segmentLoaders: { AUDIO: segmentLoader }, + mediaTypes: MediaGroups.createMediaTypes(), + blacklistCurrentPlaylist: () => blacklistCurrentPlaylistCalls++, + masterPlaylistLoader + }; + const mediaType = settings.mediaTypes[type]; + const groups = mediaType.groups; + const tracks = mediaType.tracks; + + mediaType.activeTrack = MediaGroups.activeTrack[type](type, settings); + mediaType.activeGroup = MediaGroups.activeGroup(type, settings); + mediaType.onTrackChanged = () => onTrackChangedCalls++; + + const onError = MediaGroups.onError[type](type, settings); + + groups.main = [ { id: 'en', default: true }, { id: 'fr'}, { id: 'es'} ]; + tracks.en = { id: 'en', enabed: false }; + tracks.fr = { id: 'fr', enabed: true }; + tracks.es = { id: 'es', enabed: false }; + + onError(); + + assert.equal(blacklistCurrentPlaylistCalls, 0, 'did not blacklist current playlist'); + assert.equal(onTrackChangedCalls, 1, 'called onTrackChanged after changing to default'); + assert.equal(tracks.en.enabled, true, 'enabled default track'); + assert.equal(tracks.fr.enabled, false, 'disabled active track'); + assert.equal(tracks.es.enabled, false, 'disabled track still disabled'); + assert.equal(this.env.log.warn.callCount, 1, 'logged a warning'); + this.env.log.warn.callCount = 0; + + onError(); + + assert.equal(blacklistCurrentPlaylistCalls, 1, 'blacklist current playlist'); + assert.equal(onTrackChangedCalls, 1, 'did not call onTrackChanged after blacklist'); + assert.equal(tracks.en.enabled, true, 'default track still enabled'); + assert.equal(tracks.fr.enabled, false, 'disabled track still disabled'); + assert.equal(tracks.es.enabled, false, 'disabled track still disabled'); + assert.equal(this.env.log.warn.callCount, 0, 'no warning logged'); + } + ); + + QUnit.test('disables subtitle track when an error is encountered', function(assert) { + let onTrackChangedCalls = 0; + const type = 'SUBTITLES'; const segmentLoader = { abort() {}, pause() {} }; - const masterPlaylistLoader = { - media() { - return { attributes: { AUDIO: 'main' } }; - } - }; const settings = { - segmentLoaders: { AUDIO: segmentLoader }, - mediaTypes: MediaGroups.createMediaTypes(), - blacklistCurrentPlaylist: () => blacklistCurrentPlaylistCalls++, - masterPlaylistLoader + segmentLoaders: { SUBTITLES: segmentLoader }, + mediaTypes: MediaGroups.createMediaTypes() }; const mediaType = settings.mediaTypes[type]; - const groups = mediaType.groups; const tracks = mediaType.tracks; mediaType.activeTrack = MediaGroups.activeTrack[type](type, settings); - mediaType.activeGroup = MediaGroups.activeGroup(type, settings); mediaType.onTrackChanged = () => onTrackChangedCalls++; const onError = MediaGroups.onError[type](type, settings); - groups.main = [ { id: 'en', default: true }, { id: 'fr'}, { id: 'es'} ]; - tracks.en = { id: 'en', enabed: false }; - tracks.fr = { id: 'fr', enabed: true }; - tracks.es = { id: 'es', enabed: false }; + tracks.en = { id: 'en', mode: 'disabled' }; + tracks.fr = { id: 'fr', mode: 'disabled' }; + tracks.es = { id: 'es', mode: 'showing' }; onError(); - assert.equal(blacklistCurrentPlaylistCalls, 0, 'did not blacklist current playlist'); - assert.equal(onTrackChangedCalls, 1, 'called onTrackChanged after changing to default'); - assert.equal(tracks.en.enabled, true, 'enabled default track'); - assert.equal(tracks.fr.enabled, false, 'disabled active track'); - assert.equal(tracks.es.enabled, false, 'disabled track still disabled'); + assert.equal(onTrackChangedCalls, 1, 'called onTrackChanged after disabling track'); + assert.equal(tracks.en.mode, 'disabled', 'disabled track still disabled'); + assert.equal(tracks.fr.mode, 'disabled', 'disabled track still disabled'); + assert.equal(tracks.es.mode, 'disabled', 'disabled active track'); assert.equal(this.env.log.warn.callCount, 1, 'logged a warning'); this.env.log.warn.callCount = 0; + }); - onError(); - - assert.equal(blacklistCurrentPlaylistCalls, 1, 'blacklist current playlist'); - assert.equal(onTrackChangedCalls, 1, 'did not call onTrackChanged after blacklist'); - assert.equal(tracks.en.enabled, true, 'default track still enabled'); - assert.equal(tracks.fr.enabled, false, 'disabled track still disabled'); - assert.equal(tracks.es.enabled, false, 'disabled track still disabled'); - assert.equal(this.env.log.warn.callCount, 0, 'no warning logged'); - } -); - -QUnit.test('disables subtitle track when an error is encountered', function(assert) { - let onTrackChangedCalls = 0; - const type = 'SUBTITLES'; - const segmentLoader = { abort() {}, pause() {} }; - const settings = { - segmentLoaders: { SUBTITLES: segmentLoader }, - mediaTypes: MediaGroups.createMediaTypes() - }; - const mediaType = settings.mediaTypes[type]; - const tracks = mediaType.tracks; - - mediaType.activeTrack = MediaGroups.activeTrack[type](type, settings); - mediaType.onTrackChanged = () => onTrackChangedCalls++; - - const onError = MediaGroups.onError[type](type, settings); - - tracks.en = { id: 'en', mode: 'disabled' }; - tracks.fr = { id: 'fr', mode: 'disabled' }; - tracks.es = { id: 'es', mode: 'showing' }; - - onError(); - - assert.equal(onTrackChangedCalls, 1, 'called onTrackChanged after disabling track'); - assert.equal(tracks.en.mode, 'disabled', 'disabled track still disabled'); - assert.equal(tracks.fr.mode, 'disabled', 'disabled track still disabled'); - assert.equal(tracks.es.mode, 'disabled', 'disabled active track'); - assert.equal(this.env.log.warn.callCount, 1, 'logged a warning'); - this.env.log.warn.callCount = 0; -}); - -QUnit.test('setupListeners adds correct playlist loader listeners', function(assert) { - const settings = { - tech: {}, - requestOptions: {}, - segmentLoaders: { - AUDIO: {}, - SUBTITLES: {} - }, - mediaTypes: MediaGroups.createMediaTypes() - }; - const listeners = []; - const on = (event, cb) => listeners.push([event, cb]); - const playlistLoader = { on }; - let type = 'SUBTITLES'; - - MediaGroups.setupListeners[type](type, playlistLoader, settings); + QUnit.module('setupListeners', sharedHooks); + QUnit.test('setupListeners adds correct playlist loader listeners', function(assert) { + const settings = { + tech: {}, + requestOptions: {}, + segmentLoaders: { + AUDIO: {}, + SUBTITLES: {} + }, + mediaTypes: MediaGroups.createMediaTypes() + }; + const listeners = []; + const on = (event, cb) => listeners.push([event, cb]); + const playlistLoader = { on }; + let type = 'SUBTITLES'; + + MediaGroups.setupListeners[type](type, playlistLoader, settings); + + assert.equal(listeners.length, 3, 'setup 3 event listeners'); + assert.equal(listeners[0][0], 'loadedmetadata', 'setup loadedmetadata listener'); + assert.equal(typeof listeners[0][1], 'function', 'setup loadedmetadata listener'); + assert.equal(listeners[1][0], 'loadedplaylist', 'setup loadedmetadata listener'); + assert.equal(typeof listeners[1][1], 'function', 'setup loadedmetadata listener'); + assert.equal(listeners[2][0], 'error', 'setup loadedmetadata listener'); + assert.equal(typeof listeners[2][1], 'function', 'setup loadedmetadata listener'); + + listeners.length = 0; + + type = 'AUDIO'; + + MediaGroups.setupListeners[type](type, playlistLoader, settings); + + assert.equal(listeners.length, 3, 'setup 3 event listeners'); + assert.equal(listeners[0][0], 'loadedmetadata', 'setup loadedmetadata listener'); + assert.equal(typeof listeners[0][1], 'function', 'setup loadedmetadata listener'); + assert.equal(listeners[1][0], 'loadedplaylist', 'setup loadedmetadata listener'); + assert.equal(typeof listeners[1][1], 'function', 'setup loadedmetadata listener'); + assert.equal(listeners[2][0], 'error', 'setup loadedmetadata listener'); + assert.equal(typeof listeners[2][1], 'function', 'setup loadedmetadata listener'); + + listeners.length = 0; + + MediaGroups.setupListeners[type](type, null, settings); + + assert.equal(listeners.length, 0, 'no event listeners setup when no playlist loader'); + }); + + QUnit.module('initialize', { + beforeEach(assert) { + this.mediaTypes = MediaGroups.createMediaTypes(); + this.master = { + mediaGroups: { + 'AUDIO': {}, + 'SUBTITLES': {}, + 'CLOSED-CAPTIONS': {} + }, + playlists: [] + }; + this.settings = { + mode: 'html5', + masterPlaylistLoader: {master: this.master}, + vhs: {}, + tech: { + addRemoteTextTrack(track) { + return { track }; + } + }, + segmentLoaders: { + AUDIO: { on() {} }, + SUBTITLES: { on() {} } + }, + requestOptions: { withCredentials: false, timeout: 10 }, + master: this.master, + mediaTypes: this.mediaTypes, + blacklistCurrentPlaylist() {}, + sourceType: 'hls' + }; + } + }); + + QUnit.test( + 'initialize audio forces default track when no audio groups provided', + function(assert) { + const type = 'AUDIO'; + + MediaGroups.initialize[type](type, this.settings); + + assert.deepEqual( + this.master.mediaGroups[type], + { main: { default: { default: true} } }, 'forced default audio group' + ); + assert.deepEqual( + this.mediaTypes[type].groups, + { main: [ { id: 'default', playlistLoader: null, default: true } ] }, + 'creates group properties and no playlist loader' + ); + assert.ok(this.mediaTypes[type].tracks.default, 'created default track'); + } + ); - assert.equal(listeners.length, 3, 'setup 3 event listeners'); - assert.equal(listeners[0][0], 'loadedmetadata', 'setup loadedmetadata listener'); - assert.equal(typeof listeners[0][1], 'function', 'setup loadedmetadata listener'); - assert.equal(listeners[1][0], 'loadedplaylist', 'setup loadedmetadata listener'); - assert.equal(typeof listeners[1][1], 'function', 'setup loadedmetadata listener'); - assert.equal(listeners[2][0], 'error', 'setup loadedmetadata listener'); - assert.equal(typeof listeners[2][1], 'function', 'setup loadedmetadata listener'); + QUnit.test( + 'initialize audio correctly generates tracks and playlist loaders', + function(assert) { + const type = 'AUDIO'; + + this.master.playlists = [ + {resolvedUri: 'video/fr.m3u8', attributes: {AUDIO: 'aud1', CODECS: 'avc1.4d400d'}} + ]; + this.master.mediaGroups[type].aud1 = { + en: { default: true, language: 'en' }, + fr: { default: false, language: 'fr', resolvedUri: 'aud1/fr.m3u8' } + }; + this.master.mediaGroups[type].aud2 = { + en: { default: true, language: 'en' }, + fr: { default: false, language: 'fr', resolvedUri: 'aud2/fr.m3u8' } + }; - listeners.length = 0; + MediaGroups.initialize[type](type, this.settings); + + assert.notOk(this.master.mediaGroups[type].main, 'no default main group added'); + assert.deepEqual( + this.mediaTypes[type].groups, + { + aud1: [ + { id: 'en', default: true, language: 'en', playlistLoader: null }, + { id: 'fr', default: false, language: 'fr', resolvedUri: 'aud1/fr.m3u8', + // just so deepEqual passes since there is no other way to get the object + // reference for the playlist loader. Assertions below will confirm that this is + // not null. + playlistLoader: this.mediaTypes[type].groups.aud1[1].playlistLoader } + ], + aud2: [ + { id: 'en', default: true, language: 'en', playlistLoader: null }, + { id: 'fr', default: false, language: 'fr', resolvedUri: 'aud2/fr.m3u8', + // just so deepEqual passes since there is no other way to get the object + // reference for the playlist loader. Assertions below will confirm that this is + // not null. + playlistLoader: this.mediaTypes[type].groups.aud2[1].playlistLoader } + ] + }, 'creates group properties' + ); + assert.ok( + this.mediaTypes[type].groups.aud1[1].playlistLoader, + 'playlistLoader created for non muxed audio group' + ); + assert.ok( + this.mediaTypes[type].groups.aud2[1].playlistLoader, + 'playlistLoader created for non muxed audio group' + ); + assert.ok(this.mediaTypes[type].tracks.en, 'created audio track'); + assert.ok(this.mediaTypes[type].tracks.fr, 'created audio track'); + } + ); - type = 'AUDIO'; + QUnit.test( + 'initialize subtitles correctly generates tracks and playlist loaders', + function(assert) { + const type = 'SUBTITLES'; - MediaGroups.setupListeners[type](type, playlistLoader, settings); + this.master.mediaGroups[type].sub1 = { + 'en': { language: 'en', default: true, resolvedUri: 'sub1/en.m3u8' }, + 'en-forced': { language: 'en', resolvedUri: 'sub1/en-forced.m3u8', forced: true }, + 'fr': { language: 'fr', resolvedUri: 'sub1/fr.m3u8' } + }; + this.master.mediaGroups[type].sub2 = { + 'en': { language: 'en', resolvedUri: 'sub2/en.m3u8' }, + 'en-forced': { language: 'en', resolvedUri: 'sub2/en-forced.m3u8', forced: true }, + 'fr': { language: 'fr', resolvedUri: 'sub2/fr.m3u8' } + }; - assert.equal(listeners.length, 3, 'setup 3 event listeners'); - assert.equal(listeners[0][0], 'loadedmetadata', 'setup loadedmetadata listener'); - assert.equal(typeof listeners[0][1], 'function', 'setup loadedmetadata listener'); - assert.equal(listeners[1][0], 'loadedplaylist', 'setup loadedmetadata listener'); - assert.equal(typeof listeners[1][1], 'function', 'setup loadedmetadata listener'); - assert.equal(listeners[2][0], 'error', 'setup loadedmetadata listener'); - assert.equal(typeof listeners[2][1], 'function', 'setup loadedmetadata listener'); + MediaGroups.initialize[type](type, this.settings); + + assert.deepEqual( + this.mediaTypes[type].groups, + { + sub1: [ + { id: 'en', language: 'en', default: true, resolvedUri: 'sub1/en.m3u8', + playlistLoader: this.mediaTypes[type].groups.sub1[0].playlistLoader }, + { id: 'fr', language: 'fr', resolvedUri: 'sub1/fr.m3u8', + playlistLoader: this.mediaTypes[type].groups.sub1[1].playlistLoader } + ], + sub2: [ + { id: 'en', language: 'en', resolvedUri: 'sub2/en.m3u8', + playlistLoader: this.mediaTypes[type].groups.sub2[0].playlistLoader }, + { id: 'fr', language: 'fr', resolvedUri: 'sub2/fr.m3u8', + playlistLoader: this.mediaTypes[type].groups.sub2[1].playlistLoader } + ] + }, 'creates group properties' + ); + assert.ok( + this.mediaTypes[type].groups.sub1[0].playlistLoader, + 'playlistLoader created' + ); + assert.ok( + this.mediaTypes[type].groups.sub1[1].playlistLoader, + 'playlistLoader created' + ); + assert.ok( + this.mediaTypes[type].groups.sub2[0].playlistLoader, + 'playlistLoader created' + ); + assert.ok( + this.mediaTypes[type].groups.sub2[1].playlistLoader, + 'playlistLoader created' + ); + assert.ok(this.mediaTypes[type].tracks.en, 'created text track'); + assert.equal(this.mediaTypes[type].tracks.en.default, undefined, 'No autoselect, no default'); + assert.ok(this.mediaTypes[type].tracks.fr, 'created text track'); + } + ); - listeners.length = 0; + QUnit.test( + 'initialize subtitles correctly with auto select', + function(assert) { + const type = 'SUBTITLES'; - MediaGroups.setupListeners[type](type, null, settings); + this.master.mediaGroups[type].sub1 = { + 'en': { language: 'en', default: true, autoselect: true, resolvedUri: 'sub1/en.m3u8' }, + 'en-forced': { language: 'en', resolvedUri: 'sub1/en-forced.m3u8', forced: true }, + 'fr': { language: 'fr', resolvedUri: 'sub1/fr.m3u8' } + }; + this.master.mediaGroups[type].sub2 = { + 'en': { language: 'en', resolvedUri: 'sub2/en.m3u8' }, + 'en-forced': { language: 'en', resolvedUri: 'sub2/en-forced.m3u8', forced: true }, + 'fr': { language: 'fr', resolvedUri: 'sub2/fr.m3u8' } + }; - assert.equal(listeners.length, 0, 'no event listeners setup when no playlist loader'); -}); + MediaGroups.initialize[type](type, this.settings); -QUnit.module('MediaGroups - initialize', { - beforeEach(assert) { - this.mediaTypes = MediaGroups.createMediaTypes(); - this.master = { - mediaGroups: { - 'AUDIO': {}, - 'SUBTITLES': {}, - 'CLOSED-CAPTIONS': {} - }, - playlists: [] - }; - this.settings = { - mode: 'html5', - vhs: {}, - tech: { - addRemoteTextTrack(track) { - return { track }; - } - }, - segmentLoaders: { - AUDIO: { on() {} }, - SUBTITLES: { on() {} } - }, - requestOptions: { withCredentials: false, timeout: 10 }, - master: this.master, - mediaTypes: this.mediaTypes, - blacklistCurrentPlaylist() {}, - sourceType: 'hls' - }; - } -}); + assert.equal(this.mediaTypes[type].tracks.en.default, true, 'en track auto selected'); + } + ); -QUnit.test( - 'initialize audio forces default track when no audio groups provided', - function(assert) { - const type = 'AUDIO'; + QUnit.test( + 'initialize closed-captions correctly generates tracks and NO loaders', + function(assert) { + const type = 'CLOSED-CAPTIONS'; - MediaGroups.initialize[type](type, this.settings); + this.master.mediaGroups[type].CCs = { + en608: { language: 'en', default: true, autoselect: true, instreamId: 'CC1' }, + en708: { language: 'en', instreamId: 'SERVICE1' }, + fr608: { language: 'fr', instreamId: 'CC3' }, + fr708: { language: 'fr', instreamId: 'SERVICE3' } + }; - assert.deepEqual( - this.master.mediaGroups[type], - { main: { default: { default: true } } }, 'forced default audio group' - ); - assert.deepEqual( - this.mediaTypes[type].groups, - { main: [ { id: 'default', playlistLoader: null, default: true } ] }, - 'creates group properties and no playlist loader' - ); - assert.ok(this.mediaTypes[type].tracks.default, 'created default track'); - } -); + MediaGroups.initialize[type](type, this.settings); + + assert.deepEqual( + this.mediaTypes[type].groups, + { + CCs: [ + { id: 'en608', default: true, autoselect: true, language: 'en', instreamId: 'CC1' }, + { id: 'fr608', language: 'fr', instreamId: 'CC3' } + ] + }, 'creates group properties' + ); + assert.ok(this.mediaTypes[type].tracks.en608, 'created text track'); + assert.ok(this.mediaTypes[type].tracks.fr608, 'created text track'); + assert.equal(this.mediaTypes[type].tracks.en608.default, true, 'en608 track auto selected'); + } + ); -QUnit.test( - 'initialize audio correctly generates tracks and playlist loaders', - function(assert) { - const type = 'AUDIO'; + QUnit.test('initialize audio correctly uses HLS source type', function(assert) { - this.master.mediaGroups[type].aud1 = { + this.master.playlists = [ + {resolvedUri: 'video/fr.m3u8', attributes: {AUDIO: 'aud1', CODECS: 'avc1.4d400d'}} + ]; + this.master.mediaGroups.AUDIO.aud1 = { en: { default: true, language: 'en' }, fr: { default: false, language: 'fr', resolvedUri: 'aud1/fr.m3u8' } }; - this.master.mediaGroups[type].aud2 = { - en: { default: true, language: 'en' }, - fr: { default: false, language: 'fr', resolvedUri: 'aud2/fr.m3u8' } - }; + this.settings.sourceType = 'hls'; - MediaGroups.initialize[type](type, this.settings); + MediaGroups.initialize.AUDIO('AUDIO', this.settings); - assert.notOk(this.master.mediaGroups[type].main, 'no default main group added'); - assert.deepEqual( - this.mediaTypes[type].groups, - { - aud1: [ - { id: 'en', default: true, language: 'en', playlistLoader: null }, - { id: 'fr', default: false, language: 'fr', resolvedUri: 'aud1/fr.m3u8', - // just so deepEqual passes since there is no other way to get the object - // reference for the playlist loader. Assertions below will confirm that this is - // not null. - playlistLoader: this.mediaTypes[type].groups.aud1[1].playlistLoader } - ], - aud2: [ - { id: 'en', default: true, language: 'en', playlistLoader: null }, - { id: 'fr', default: false, language: 'fr', resolvedUri: 'aud2/fr.m3u8', - // just so deepEqual passes since there is no other way to get the object - // reference for the playlist loader. Assertions below will confirm that this is - // not null. - playlistLoader: this.mediaTypes[type].groups.aud2[1].playlistLoader } - ] - }, 'creates group properties' - ); - assert.ok( - this.mediaTypes[type].groups.aud1[1].playlistLoader, - 'playlistLoader created for non muxed audio group' + assert.notOk( + this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, + 'no playlist loader because muxed (no URI)' ); assert.ok( - this.mediaTypes[type].groups.aud2[1].playlistLoader, - 'playlistLoader created for non muxed audio group' + this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader instanceof PlaylistLoader, + 'playlist loader is an HLS playlist loader' ); - assert.ok(this.mediaTypes[type].tracks.en, 'created audio track'); - assert.ok(this.mediaTypes[type].tracks.fr, 'created audio track'); - } -); - -QUnit.test( - 'initialize subtitles correctly generates tracks and playlist loaders', - function(assert) { - const type = 'SUBTITLES'; + }); - this.master.mediaGroups[type].sub1 = { - 'en': { language: 'en', default: true, resolvedUri: 'sub1/en.m3u8' }, - 'en-forced': { language: 'en', resolvedUri: 'sub1/en-forced.m3u8', forced: true }, - 'fr': { language: 'fr', resolvedUri: 'sub1/fr.m3u8' } - }; - this.master.mediaGroups[type].sub2 = { - 'en': { language: 'en', resolvedUri: 'sub2/en.m3u8' }, - 'en-forced': { language: 'en', resolvedUri: 'sub2/en-forced.m3u8', forced: true }, - 'fr': { language: 'fr', resolvedUri: 'sub2/fr.m3u8' } + QUnit.test('no audio loader for audio only with duplicated audio groups', function(assert) { + this.master.mediaGroups.AUDIO.aud1 = { + en: { default: true, language: 'en', resolvedUri: 'en.m3u8'} }; - MediaGroups.initialize[type](type, this.settings); + this.settings.sourceType = 'hls'; - assert.deepEqual( - this.mediaTypes[type].groups, - { - sub1: [ - { id: 'en', language: 'en', default: true, resolvedUri: 'sub1/en.m3u8', - playlistLoader: this.mediaTypes[type].groups.sub1[0].playlistLoader }, - { id: 'fr', language: 'fr', resolvedUri: 'sub1/fr.m3u8', - playlistLoader: this.mediaTypes[type].groups.sub1[1].playlistLoader } - ], - sub2: [ - { id: 'en', language: 'en', resolvedUri: 'sub2/en.m3u8', - playlistLoader: this.mediaTypes[type].groups.sub2[0].playlistLoader }, - { id: 'fr', language: 'fr', resolvedUri: 'sub2/fr.m3u8', - playlistLoader: this.mediaTypes[type].groups.sub2[1].playlistLoader } - ] - }, 'creates group properties' - ); - assert.ok( - this.mediaTypes[type].groups.sub1[0].playlistLoader, - 'playlistLoader created' - ); - assert.ok( - this.mediaTypes[type].groups.sub1[1].playlistLoader, - 'playlistLoader created' - ); - assert.ok( - this.mediaTypes[type].groups.sub2[0].playlistLoader, - 'playlistLoader created' - ); - assert.ok( - this.mediaTypes[type].groups.sub2[1].playlistLoader, - 'playlistLoader created' - ); - assert.ok(this.mediaTypes[type].tracks.en, 'created text track'); - assert.equal(this.mediaTypes[type].tracks.en.default, undefined, 'No autoselect, no default'); - assert.ok(this.mediaTypes[type].tracks.fr, 'created text track'); - } -); + this.settings.master.playlists = [ + {resolvedUri: 'en.m3u8', attributes: {AUDIO: 'aud1', CODECS: 'mp4a.40.2'}} + ]; + MediaGroups.initialize.AUDIO('AUDIO', this.settings); -QUnit.test( - 'initialize subtitles correctly with auto select', - function(assert) { - const type = 'SUBTITLES'; + assert.notOk( + this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, + 'no loader as audio group is the same as main renditions' + ); + }); - this.master.mediaGroups[type].sub1 = { - 'en': { language: 'en', default: true, autoselect: true, resolvedUri: 'sub1/en.m3u8' }, - 'en-forced': { language: 'en', resolvedUri: 'sub1/en-forced.m3u8', forced: true }, - 'fr': { language: 'fr', resolvedUri: 'sub1/fr.m3u8' } - }; - this.master.mediaGroups[type].sub2 = { - 'en': { language: 'en', resolvedUri: 'sub2/en.m3u8' }, - 'en-forced': { language: 'en', resolvedUri: 'sub2/en-forced.m3u8', forced: true }, - 'fr': { language: 'fr', resolvedUri: 'sub2/fr.m3u8' } + QUnit.test('audio loader created with audio group duplicated as audio only rendition', function(assert) { + this.master.mediaGroups.AUDIO.aud1 = { + en: { default: true, language: 'en', resolvedUri: 'en.m3u8' } }; - MediaGroups.initialize[type](type, this.settings); - - assert.equal(this.mediaTypes[type].tracks.en.default, true, 'en track auto selected'); - } -); - -QUnit.test( - 'initialize closed-captions correctly generates tracks and NO loaders', - function(assert) { - const type = 'CLOSED-CAPTIONS'; - - this.master.mediaGroups[type].CCs = { - en608: { language: 'en', default: true, autoselect: true, instreamId: 'CC1' }, - en708: { language: 'en', instreamId: 'SERVICE1' }, - fr608: { language: 'fr', instreamId: 'CC3' }, - fr708: { language: 'fr', instreamId: 'SERVICE3' } - }; + this.settings.sourceType = 'hls'; - MediaGroups.initialize[type](type, this.settings); + this.settings.master.playlists = [ + {resolvedUri: 'video/en.m3u8', attributes: {AUDIO: 'aud1'}}, + {resolvedUri: 'en.m3u8', attributes: {AUDIO: 'aud1'}} + ]; + MediaGroups.initialize.AUDIO('AUDIO', this.settings); - assert.deepEqual( - this.mediaTypes[type].groups, - { - CCs: [ - { id: 'en608', default: true, autoselect: true, language: 'en', instreamId: 'CC1' }, - { id: 'fr608', language: 'fr', instreamId: 'CC3' } - ] - }, 'creates group properties' + assert.ok( + this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, + 'audio loader created' ); - assert.ok(this.mediaTypes[type].tracks.en608, 'created text track'); - assert.ok(this.mediaTypes[type].tracks.fr608, 'created text track'); - assert.equal(this.mediaTypes[type].tracks.en608.default, true, 'en608 track auto selected'); - } -); + }); -QUnit.test('initialize audio correctly uses HLS source type', function(assert) { - this.master.mediaGroups.AUDIO.aud1 = { - en: { default: true, language: 'en' }, - fr: { default: false, language: 'fr', resolvedUri: 'aud1/fr.m3u8' } - }; - this.settings.sourceType = 'hls'; - - MediaGroups.initialize.AUDIO('AUDIO', this.settings); - - assert.notOk( - this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, - 'no playlist loader because muxed (no URI)' - ); - assert.ok( - this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader instanceof PlaylistLoader, - 'playlist loader is an HLS playlist loader' - ); -}); + QUnit.test('initialize audio correctly uses DASH source type', function(assert) { + // allow async methods to resolve before next test + const done = assert.async(); -QUnit.test('no audio loader for audio only with duplicated audio groups', function(assert) { - this.master.mediaGroups.AUDIO.aud1 = { - en: { default: true, language: 'en', resolvedUri: 'en.m3u8' } - }; - - this.settings.sourceType = 'hls'; - - this.settings.master.playlists = [ - {resolvedUri: 'en.m3u8', attributes: {AUDIO: 'aud1'}} - ]; - MediaGroups.initialize.AUDIO('AUDIO', this.settings); - - assert.notOk( - this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, - 'no loader as audio group is the same as main renditions' - ); -}); - -QUnit.test('audio loader created with audio group duplicated as audio only rendition', function(assert) { - this.master.mediaGroups.AUDIO.aud1 = { - en: { default: true, language: 'en', resolvedUri: 'en.m3u8' } - }; - - this.settings.sourceType = 'hls'; - - this.settings.master.playlists = [ - {resolvedUri: 'video/en.m3u8', attributes: {AUDIO: 'aud1'}}, - {resolvedUri: 'en.m3u8', attributes: {AUDIO: 'aud1'}} - ]; - MediaGroups.initialize.AUDIO('AUDIO', this.settings); + this.master.playlists = [ + {resolvedUri: 'video/fr.m3u8', attributes: {AUDIO: 'aud1', CODECS: 'avc1.4d400d'}} + ]; - assert.ok( - this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, - 'audio loader created' - ); -}); + this.master.mediaGroups.AUDIO.aud1 = { + // playlists are resolved, no URI for DASH + // use strings as playlists to simplify test to prevent playlist object code path + // which assumes there a MastPlaylistLoader + en: { default: true, language: 'en', playlists: ['playlist-1'] }, + fr: { default: false, language: 'fr', playlists: ['playlist-2'] } + }; + this.settings.sourceType = 'dash'; -QUnit.test('initialize audio correctly uses DASH source type', function(assert) { - // allow async methods to resolve before next test - const done = assert.async(); + MediaGroups.initialize.AUDIO('AUDIO', this.settings); - this.master.mediaGroups.AUDIO.aud1 = { - // playlists are resolved, no URI for DASH - // use strings as playlists to simplify test to prevent playlist object code path - // which assumes there a MastPlaylistLoader - en: { default: true, language: 'en', playlists: ['playlist-1'] }, - fr: { default: false, language: 'fr', playlists: ['playlist-2'] } - }; - this.settings.sourceType = 'dash'; + assert.ok( + this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader instanceof DashPlaylistLoader, + 'playlist loader is a DASH playlist loader' + ); + assert.ok( + this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader instanceof DashPlaylistLoader, + 'playlist loader is a DASH playlist loader' + ); - MediaGroups.initialize.AUDIO('AUDIO', this.settings); + done(); + }); - assert.ok( - this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader instanceof DashPlaylistLoader, - 'playlist loader is a DASH playlist loader' - ); - assert.ok( - this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader instanceof DashPlaylistLoader, - 'playlist loader is a DASH playlist loader' + QUnit.test( + 'initialize audio does not create DASH playlist loader if no playlists', + function(assert) { + this.master.mediaGroups.AUDIO.aud1 = { + en: { default: true, language: 'en' }, + fr: { default: false, language: 'fr' } + }; + this.settings.sourceType = 'dash'; + + MediaGroups.initialize.AUDIO('AUDIO', this.settings); + + assert.notOk( + this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, + 'no playlist loader when misconfigured' + ); + assert.notOk( + this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader, + 'no playlist loader when misconfigured' + ); + } ); - done(); -}); - -QUnit.test( - 'initialize audio does not create DASH playlist loader if no playlists', - function(assert) { + QUnit.skip('initialize audio does not create playlist loader for alternate tracks with' + +' main stream as URI attribute', function(assert) { this.master.mediaGroups.AUDIO.aud1 = { - en: { default: true, language: 'en' }, - fr: { default: false, language: 'fr' } + en: { default: true, language: 'en', resolvedUri: 'main.m3u8' }, + fr: { default: false, language: 'fr', resolvedUri: 'audio/fr.m3u8' } }; - this.settings.sourceType = 'dash'; + this.master.playlists = [{ + attributes: { AUDIO: 'aud1' }, + resolvedUri: 'main.m3u8' + }]; MediaGroups.initialize.AUDIO('AUDIO', this.settings); assert.notOk( - this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, - 'no playlist loader when misconfigured' + this.mediaTypes.AUDIO.groups.aud1[0].resolvedUri, + 'resolvedUri proeprty deleted' ); assert.notOk( - this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader, - 'no playlist loader when misconfigured' + this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, + 'no playlist loader for alternate audio in main stream' ); - } -); - -QUnit.skip('initialize audio does not create playlist loader for alternate tracks with' + -' main stream as URI attribute', function(assert) { - this.master.mediaGroups.AUDIO.aud1 = { - en: { default: true, language: 'en', resolvedUri: 'main.m3u8' }, - fr: { default: false, language: 'fr', resolvedUri: 'audio/fr.m3u8' } - }; - this.master.playlists = [{ - attributes: { AUDIO: 'aud1' }, - resolvedUri: 'main.m3u8' - }]; - - MediaGroups.initialize.AUDIO('AUDIO', this.settings); - - assert.notOk( - this.mediaTypes.AUDIO.groups.aud1[0].resolvedUri, - 'resolvedUri proeprty deleted' - ); - assert.notOk( - this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader, - 'no playlist loader for alternate audio in main stream' - ); - assert.ok( - this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader instanceof PlaylistLoader, - 'playlist loader for alternate audio not in main stream' - ); -}); - -QUnit.test('initialize subtitles correctly uses HLS source type', function(assert) { - this.master.mediaGroups.SUBTITLES.sub1 = { - en: { language: 'en', resolvedUri: 'sub1/en.m3u8' }, - fr: { language: 'fr', resolvedUri: 'sub1/fr.m3u8' } - }; - this.settings.sourceType = 'hls'; + assert.ok( + this.mediaTypes.AUDIO.groups.aud1[1].playlistLoader instanceof PlaylistLoader, + 'playlist loader for alternate audio not in main stream' + ); + }); - MediaGroups.initialize.SUBTITLES('SUBTITLES', this.settings); + QUnit.test('initialize subtitles correctly uses HLS source type', function(assert) { + this.master.mediaGroups.SUBTITLES.sub1 = { + en: { language: 'en', resolvedUri: 'sub1/en.m3u8' }, + fr: { language: 'fr', resolvedUri: 'sub1/fr.m3u8' } + }; + this.settings.sourceType = 'hls'; - assert.ok( - this.mediaTypes.SUBTITLES.groups.sub1[0].playlistLoader instanceof PlaylistLoader, - 'playlist loader is an HLS playlist loader' - ); - assert.ok( - this.mediaTypes.SUBTITLES.groups.sub1[1].playlistLoader instanceof PlaylistLoader, - 'playlist loader is an HLS playlist loader' - ); -}); + MediaGroups.initialize.SUBTITLES('SUBTITLES', this.settings); -QUnit.test('initialize audio correctly uses vhs-json source type', function(assert) { - const manifestString = manifests.media; - const audioPlaylist = parseManifest({ manifestString }); + assert.ok( + this.mediaTypes.SUBTITLES.groups.sub1[0].playlistLoader instanceof PlaylistLoader, + 'playlist loader is an HLS playlist loader' + ); + assert.ok( + this.mediaTypes.SUBTITLES.groups.sub1[1].playlistLoader instanceof PlaylistLoader, + 'playlist loader is an HLS playlist loader' + ); + }); - this.master.mediaGroups.AUDIO.aud1 = { - en: { - default: true, - language: 'en', - playlists: [audioPlaylist] - } - }; - this.settings.sourceType = 'vhs-json'; + QUnit.test('initialize audio correctly uses vhs-json source type', function(assert) { + const manifestString = manifests.media; + const audioPlaylist = parseManifest({ manifestString }); - MediaGroups.initialize.AUDIO('AUDIO', this.settings); + this.master.playlists = [ + {resolvedUri: 'video/fr.m3u8', attributes: {AUDIO: 'aud1', CODECS: 'avc1.4d400d'}} + ]; - const playlistLoader = this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader; + this.master.mediaGroups.AUDIO.aud1 = { + en: { + default: true, + language: 'en', + playlists: [audioPlaylist] + } + }; + this.settings.sourceType = 'vhs-json'; - assert.ok( - playlistLoader instanceof PlaylistLoader, - 'playlist loader is a standard playlist loader' - ); - assert.deepEqual(playlistLoader.src, audioPlaylist, 'passed the audio playlist'); -}); + MediaGroups.initialize.AUDIO('AUDIO', this.settings); -QUnit.test('initialize subtitles correctly uses DASH source type', function(assert) { - // allow async methods to resolve before next test - const done = assert.async(); + const playlistLoader = this.mediaTypes.AUDIO.groups.aud1[0].playlistLoader; - this.master.mediaGroups.SUBTITLES.sub1 = { - // playlists are resolved, no URI for DASH - // use strings as playlists to simplify test to prevent playlist object code path - // which assumes there a MastPlaylistLoader - en: { language: 'en', playlists: ['playlist-1'] }, - fr: { language: 'fr', playlists: ['playlist-2'] } - }; - this.settings.sourceType = 'dash'; + assert.ok( + playlistLoader instanceof PlaylistLoader, + 'playlist loader is a standard playlist loader' + ); + assert.deepEqual(playlistLoader.src, audioPlaylist, 'passed the audio playlist'); + }); + + QUnit.test('initialize subtitles correctly uses DASH source type', function(assert) { + // allow async methods to resolve before next test + const done = assert.async(); + + this.master.mediaGroups.SUBTITLES.sub1 = { + // playlists are resolved, no URI for DASH + // use strings as playlists to simplify test to prevent playlist object code path + // which assumes there a MastPlaylistLoader + en: { language: 'en', playlists: ['playlist-1'] }, + fr: { language: 'fr', playlists: ['playlist-2'] } + }; + this.settings.sourceType = 'dash'; - MediaGroups.initialize.AUDIO('AUDIO', this.settings); + MediaGroups.initialize.AUDIO('AUDIO', this.settings); - MediaGroups.initialize.SUBTITLES('SUBTITLES', this.settings); + MediaGroups.initialize.SUBTITLES('SUBTITLES', this.settings); - assert.ok( - this.mediaTypes.SUBTITLES.groups.sub1[0].playlistLoader instanceof DashPlaylistLoader, - 'playlist loader is a DASH playlist loader' - ); - assert.ok( - this.mediaTypes.SUBTITLES.groups.sub1[1].playlistLoader instanceof DashPlaylistLoader, - 'playlist loader is a DASH playlist loader' - ); + assert.ok( + this.mediaTypes.SUBTITLES.groups.sub1[0].playlistLoader instanceof DashPlaylistLoader, + 'playlist loader is a DASH playlist loader' + ); + assert.ok( + this.mediaTypes.SUBTITLES.groups.sub1[1].playlistLoader instanceof DashPlaylistLoader, + 'playlist loader is a DASH playlist loader' + ); - done(); -}); + done(); + }); -QUnit.test('initialize subtitles correctly uses vhs-json source type', function(assert) { - const manifestString = manifests.subtitles; - const subtitlesPlaylist = parseManifest({ manifestString }); + QUnit.test('initialize subtitles correctly uses vhs-json source type', function(assert) { + const manifestString = manifests.subtitles; + const subtitlesPlaylist = parseManifest({ manifestString }); - this.master.mediaGroups.SUBTITLES.sub1 = { - en: { - language: 'en', - playlists: [subtitlesPlaylist] - } - }; - this.settings.sourceType = 'vhs-json'; + this.master.mediaGroups.SUBTITLES.sub1 = { + en: { + language: 'en', + playlists: [subtitlesPlaylist] + } + }; + this.settings.sourceType = 'vhs-json'; - MediaGroups.initialize.SUBTITLES('SUBTITLES', this.settings); + MediaGroups.initialize.SUBTITLES('SUBTITLES', this.settings); - const playlistLoader = this.mediaTypes.SUBTITLES.groups.sub1[0].playlistLoader; + const playlistLoader = this.mediaTypes.SUBTITLES.groups.sub1[0].playlistLoader; - assert.ok( - playlistLoader instanceof PlaylistLoader, - 'playlist loader is a standard playlist loader' - ); - assert.deepEqual( - playlistLoader.src, - subtitlesPlaylist, - 'passed the subtitles playlist' - ); + assert.ok( + playlistLoader instanceof PlaylistLoader, + 'playlist loader is a standard playlist loader' + ); + assert.deepEqual( + playlistLoader.src, + subtitlesPlaylist, + 'passed the subtitles playlist' + ); + }); }); diff --git a/test/playlist-selectors.test.js b/test/playlist-selectors.test.js index c94ac4bfb..9db0f2006 100644 --- a/test/playlist-selectors.test.js +++ b/test/playlist-selectors.test.js @@ -210,3 +210,26 @@ test('simpleSelector can not limit based on resolution information', function(as assert.equal(selectedPlaylist, master.playlists[4], 'selected a playlist based solely on bandwidth'); }); + +test('simpleSelector chooses between current audio playlists for audio only', function(assert) { + const audioPlaylists = [ + {id: 'foo'}, + {id: 'bar', attributes: {BANDWIDTH: 534216}} + ]; + const masterPlaylistController = { + getAudioTrackPlaylists_: () => audioPlaylists + }; + const master = this.vhs.playlists.master; + + master.mediaGroups = { + AUDIO: { + main: { + en: {id: 'en', playlists: audioPlaylists} + } + } + }; + + const selectedPlaylist = simpleSelector(master, Config.INITIAL_BANDWIDTH, 444, 790, false, masterPlaylistController); + + assert.equal(selectedPlaylist, audioPlaylists[1], 'selected an audio based solely on bandwidth'); +}); diff --git a/test/playlist.test.js b/test/playlist.test.js index b81523524..66ce0414c 100644 --- a/test/playlist.test.js +++ b/test/playlist.test.js @@ -1317,4 +1317,95 @@ QUnit.module('Playlist', function() { 'no segments, live delay can\'t be calculated' ); }); + + QUnit.test('playlistMatch', function(assert) { + assert.false(Playlist.playlistMatch(null, null), 'null playlists do not match'); + assert.false(Playlist.playlistMatch({}, null), 'a playlist without b'); + assert.false(Playlist.playlistMatch(null, {}), 'b playlist without a'); + + const a = {id: 'foo', uri: 'foo.m3u8', resolvedUri: 'http://example.com/foo.m3u8'}; + const b = {id: 'foo', uri: 'foo.m3u8', resolvedUri: 'http://example.com/foo.m3u8'}; + + assert.true(Playlist.playlistMatch(a, a), 'object signature match'); + + assert.true(Playlist.playlistMatch(a, b), 'id match'); + + a.id = 'bar'; + assert.true(Playlist.playlistMatch(a, b), 'resolved uri match'); + + a.resolvedUri += '?nope'; + assert.true(Playlist.playlistMatch(a, b), 'uri match'); + + a.uri += '?nope'; + + assert.false(Playlist.playlistMatch(a, b), 'no match'); + }); + + QUnit.test('isAudioOnly', function(assert) { + assert.false(Playlist.isAudioOnly({ + playlists: [{attributes: {CODECS: 'mp4a.40.2,avc1.4d400d'}}] + }), 'muxed playlist'); + + assert.false(Playlist.isAudioOnly({ + playlists: [ + {attributes: {CODECS: 'mp4a.40.2,avc1.4d400d'}}, + {attributes: {CODECS: 'avc1.4d400d'}}, + {attributes: {CODECS: 'mp4a.40.2'}} + ] + }), 'muxed, audio only, and video only'); + + assert.false(Playlist.isAudioOnly({ + mediaGroups: { + AUDIO: { + main: { + en: {id: 'en', uri: 'en'}, + es: {id: 'es', uri: 'es'} + } + } + }, + playlists: [{attributes: {CODECS: 'mp4a.40.2,avc1.4d400d'}}] + }), 'muxed and alt audio'); + + assert.true(Playlist.isAudioOnly({ + playlists: [ + {attributes: {CODECS: 'mp4a.40.2'}}, + {attributes: {CODECS: 'mp4a.40.2'}}, + {attributes: {CODECS: 'mp4a.40.2'}} + ] + }), 'audio only playlists'); + + assert.true(Playlist.isAudioOnly({ + mediaGroups: { + AUDIO: { + main: { + en: {id: 'en', uri: 'en'} + } + } + } + }), 'only audio groups, uri'); + + assert.true(Playlist.isAudioOnly({ + mediaGroups: { + AUDIO: { + main: { + en: {id: 'en', playlists: [{uri: 'foo'}]} + } + } + } + }), 'only audio groups, playlists'); + + assert.true(Playlist.isAudioOnly({ + playlists: [ + {id: 'en'} + ], + mediaGroups: { + AUDIO: { + main: { + en: {id: 'en'} + } + } + } + }), 'audio playlists that are also in groups, without codecs'); + + }); }); diff --git a/test/rendition-mixin.test.js b/test/rendition-mixin.test.js index b56334162..ec3146f55 100644 --- a/test/rendition-mixin.test.js +++ b/test/rendition-mixin.test.js @@ -48,7 +48,10 @@ const makeMockPlaylist = function(options) { return playlist; }; -const makeMockVhsHandler = function(playlistOptions, handlerOptions) { +const makeMockVhsHandler = function(playlistOptions = [], handlerOptions = {}, master = {}) { + const vhsHandler = { + options_: handlerOptions + }; const mpc = { fastQualityChange_: () => { mpc.fastQualityChange_.calls++; @@ -57,20 +60,24 @@ const makeMockVhsHandler = function(playlistOptions, handlerOptions) { mpc.smoothQualityChange_.calls++; }, master: () => { - return {}; + return vhsHandler.playlists.master; + }, + getAudioTrackPlaylists_: () => { + return []; } }; mpc.fastQualityChange_.calls = 0; mpc.smoothQualityChange_.calls = 0; - const vhsHandler = { - masterPlaylistController_: mpc, - options_: handlerOptions || {} - }; - + vhsHandler.masterPlaylistController_ = mpc; vhsHandler.playlists = new videojs.EventTarget(); - vhsHandler.playlists.master = { playlists: [] }; + + vhsHandler.playlists.master = master; + + if (!vhsHandler.playlists.master.playlists) { + vhsHandler.playlists.master.playlists = []; + } playlistOptions.forEach((playlist, i) => { vhsHandler.playlists.master.playlists[i] = makeMockPlaylist(playlist); @@ -398,17 +405,10 @@ QUnit.test('codecs attribute is exposed on renditions when available', function( }); QUnit.test('codecs attribute gets codecs from master', function(assert) { - const vhsHandler = makeMockVhsHandler([ + const vhsHandler = makeMockVhsHandler( + [{bandwidth: 0, uri: 'media0.m3u8', audio: 'a1'}], + {}, { - bandwidth: 0, - uri: 'media0.m3u8', - audio: 'a1' - } - ]); - const mpc = vhsHandler.masterPlaylistController_; - - mpc.master = () => { - return { mediaGroups: { AUDIO: { a1: { @@ -422,8 +422,8 @@ QUnit.test('codecs attribute gets codecs from master', function(assert) { } } } - }; - }; + } + ); RenditionMixin(vhsHandler);