Browse Source

MediaGroups: various bug fixes and refactor (#1243)

* removes the Firefox 48 check for for supporting a change in audio info
* Fix delayed switching between audio tracks and intermittent desync.
pull/6/head
Matthew Neil 8 years ago
committed by GitHub
parent
commit
24894e62ec
  1. 597
      src/master-playlist-controller.js
  2. 751
      src/media-groups.js
  3. 28
      src/playlist-loader.js
  4. 12
      src/sync-controller.js
  5. 53
      src/videojs-contrib-hls.js
  6. 12
      src/vtt-segment-loader.js
  7. 299
      test/master-playlist-controller.test.js
  8. 749
      test/media-groups.test.js
  9. 146
      test/videojs-contrib-hls.test.js

597
src/master-playlist-controller.js

@ -13,6 +13,7 @@ import worker from 'webworkify';
import Decrypter from './decrypter-worker';
import Config from './config';
import { parseCodecs } from './util/codecs.js';
import { createMediaTypes, setupMediaGroups } from './media-groups';
const ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2;
@ -41,33 +42,6 @@ const sumLoaderStat = function(stat) {
this.mainSegmentLoader_[stat];
};
/**
* determine if an object a is differnt from
* and object b. both only having one dimensional
* properties
*
* @param {Object} a object one
* @param {Object} b object two
* @return {Boolean} if the object has changed or not
*/
const objectChanged = function(a, b) {
if (typeof a !== typeof b) {
return true;
}
// if we have a different number of elements
// something has changed
if (Object.keys(a).length !== Object.keys(b).length) {
return true;
}
for (let prop in a) {
if (a[prop] !== b[prop]) {
return true;
}
}
return false;
};
/**
* Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the
* standard `avc1.<hhhhhh>`.
@ -284,13 +258,9 @@ export class MasterPlaylistController extends videojs.EventTarget {
timeout: null
};
this.audioGroups_ = {};
this.subtitleGroups_ = { groups: {}, tracks: {} };
this.closedCaptionGroups_ = { groups: {}, tracks: {} };
this.mediaTypes_ = createMediaTypes();
this.mediaSource = new videojs.MediaSource({ mode });
this.audioinfo_ = null;
this.mediaSource.on('audioinfo', this.handleAudioinfoUpdate_.bind(this));
// load the media source into the player
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
@ -323,8 +293,6 @@ export class MasterPlaylistController extends videojs.EventTarget {
// setup playlist loaders
this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials);
this.setupMasterPlaylistLoaderListeners_();
this.audioPlaylistLoader_ = null;
this.subtitlePlaylistLoader_ = null;
// setup segment loaders
// combined audio/video or just video when alternate audio track is selected
@ -381,14 +349,23 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.mainSegmentLoader_.load();
}
this.fillAudioTracks_();
this.setupAudio();
this.fillSubtitleTracks_();
this.setupSubtitles();
setupMediaGroups({
segmentLoaders: {
AUDIO: this.audioSegmentLoader_,
SUBTITLES: this.subtitleSegmentLoader_,
main: this.mainSegmentLoader_
},
tech: this.tech_,
requestOptions: this.requestOptions_,
masterPlaylistLoader: this.masterPlaylistLoader_,
mode: this.mode_,
hls: this.hls_,
master: this.master(),
mediaTypes: this.mediaTypes_,
blacklistCurrentPlaylist: this.blacklistCurrentPlaylist.bind(this)
});
this.triggerPresenceUsage_(this.master(), media);
this.fillClosedCaptionTracks_();
try {
this.setupSourceBuffers_();
@ -398,7 +375,6 @@ export class MasterPlaylistController extends videojs.EventTarget {
}
this.setupFirstPlay();
this.trigger('audioupdate');
this.trigger('selectedinitialmedia');
});
@ -476,8 +452,6 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.masterPlaylistLoader_.on('mediachange', () => {
let media = this.masterPlaylistLoader_.media();
let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000;
let activeAudioGroup;
let activeTrack;
// If we don't have any more available playlists, we don't want to
// timeout the request.
@ -494,16 +468,6 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.mainSegmentLoader_.playlist(media, this.requestOptions_);
this.mainSegmentLoader_.load();
// if the audio group has changed, a new audio track has to be
// enabled
activeAudioGroup = this.activeAudioGroup();
activeTrack = activeAudioGroup.filter((track) => track.enabled)[0];
if (!activeTrack) {
this.mediaGroupChanged();
this.trigger('audioupdate');
}
this.setupSubtitles();
this.tech_.trigger({
type: 'mediachange',
bubbles: true
@ -651,58 +615,6 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.audioSegmentLoader_.on('ended', () => {
this.onEndOfStream();
});
this.audioSegmentLoader_.on('error', () => {
videojs.log.warn('Problem encountered with the current alternate audio track' +
'. Switching back to default.');
this.audioSegmentLoader_.abort();
this.audioPlaylistLoader_ = null;
this.setupAudio();
});
this.subtitleSegmentLoader_.on('error', this.handleSubtitleError_.bind(this));
}
handleAudioinfoUpdate_(event) {
if (Hls.supportsAudioInfoChange_() ||
!this.audioInfo_ ||
!objectChanged(this.audioInfo_, event.info)) {
this.audioInfo_ = event.info;
return;
}
let error = 'had different audio properties (channels, sample rate, etc.) ' +
'or changed in some other way. This behavior is currently ' +
'unsupported in Firefox 48 and below due to an issue: \n\n' +
'https://bugzilla.mozilla.org/show_bug.cgi?id=1247138\n\n';
let enabledIndex =
this.activeAudioGroup()
.map((track) => track.enabled)
.indexOf(true);
let enabledTrack = this.activeAudioGroup()[enabledIndex];
let defaultTrack = this.activeAudioGroup().filter((track) => {
return track.properties_ && track.properties_.default;
})[0];
// they did not switch audiotracks
// blacklist the current playlist
if (!this.audioPlaylistLoader_) {
error = `The rendition that we tried to switch to ${error}` +
'Unfortunately that means we will have to blacklist ' +
'the current playlist and switch to another. Sorry!';
this.blacklistCurrentPlaylist();
} else {
error = `The audio track '${enabledTrack.label}' that we tried to ` +
`switch to ${error} Unfortunately this means we will have to ` +
`return you to the main track '${defaultTrack.label}'. Sorry!`;
defaultTrack.enabled = true;
this.activeAudioGroup().splice(enabledIndex, 1);
this.trigger('audioupdate');
}
videojs.log.warn(error);
this.setupAudio();
}
mediaSecondsLoaded_() {
@ -710,458 +622,19 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.mainSegmentLoader_.mediaSecondsLoaded);
}
/**
* fill our internal list of HlsAudioTracks with data from
* the master playlist or use a default
*
* @private
*/
fillAudioTracks_() {
let master = this.master();
let mediaGroups = master.mediaGroups || {};
// force a default if we have none or we are not
// in html5 mode (the only mode to support more than one
// audio track)
if (!mediaGroups ||
!mediaGroups.AUDIO ||
Object.keys(mediaGroups.AUDIO).length === 0 ||
this.mode_ !== 'html5') {
// "main" audio group, track name "default"
mediaGroups.AUDIO = { main: { default: { default: true }}};
}
for (let mediaGroup in mediaGroups.AUDIO) {
if (!this.audioGroups_[mediaGroup]) {
this.audioGroups_[mediaGroup] = [];
}
for (let label in mediaGroups.AUDIO[mediaGroup]) {
let properties = mediaGroups.AUDIO[mediaGroup][label];
let track = new videojs.AudioTrack({
id: label,
kind: this.audioTrackKind_(properties),
enabled: false,
language: properties.language,
label
});
track.properties_ = properties;
this.audioGroups_[mediaGroup].push(track);
}
}
// enable the default active track
(this.activeAudioGroup().filter((audioTrack) => {
return audioTrack.properties_.default;
})[0] || this.activeAudioGroup()[0]).enabled = true;
}
/**
* Convert the properties of an HLS track into an audioTrackKind.
*
* @private
*/
audioTrackKind_(properties) {
let kind = properties.default ? 'main' : 'alternative';
if (properties.characteristics &&
properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
kind = 'main-desc';
}
return kind;
}
/**
* fill our internal list of Subtitle Tracks with data from
* the master playlist or use a default
*
* @private
*/
fillSubtitleTracks_() {
let master = this.master();
let mediaGroups = master.mediaGroups || {};
for (let mediaGroup in mediaGroups.SUBTITLES) {
if (!this.subtitleGroups_.groups[mediaGroup]) {
this.subtitleGroups_.groups[mediaGroup] = [];
}
for (let label in mediaGroups.SUBTITLES[mediaGroup]) {
let properties = mediaGroups.SUBTITLES[mediaGroup][label];
if (!properties.forced) {
this.subtitleGroups_.groups[mediaGroup].push(
videojs.mergeOptions({ id: label }, properties));
if (typeof this.subtitleGroups_.tracks[label] === 'undefined') {
let track = this.tech_.addRemoteTextTrack({
id: label,
kind: 'subtitles',
enabled: false,
language: properties.language,
label
}, false).track;
this.subtitleGroups_.tracks[label] = track;
}
}
}
}
// Do not enable a default subtitle track. Wait for user interaction instead.
}
/**
* fill our internal list of Captions Tracks with data from
* the master playlist or use a default
*
* @private
*/
fillClosedCaptionTracks_() {
let master = this.master();
let mediaGroups = master.mediaGroups || {};
for (let mediaGroup in mediaGroups['CLOSED-CAPTIONS']) {
if (!this.closedCaptionGroups_.groups[mediaGroup]) {
this.closedCaptionGroups_.groups[mediaGroup] = [];
}
for (let label in mediaGroups['CLOSED-CAPTIONS'][mediaGroup]) {
let properties = mediaGroups['CLOSED-CAPTIONS'][mediaGroup][label];
// We only support CEA608 captions for now, so ignore anything that
// doesn't use a CCx INSTREAM-ID
if (!properties.instreamId.match(/CC\d/)) {
continue;
}
this.closedCaptionGroups_.groups[mediaGroup].push(
videojs.mergeOptions({ id: label }, properties));
if (typeof this.closedCaptionGroups_.tracks[label] === 'undefined') {
let track = this.tech_.addRemoteTextTrack({
id: properties.instreamId,
kind: 'captions',
enabled: false,
language: properties.language,
label
}, false).track;
this.closedCaptionGroups_.tracks[label] = track;
}
}
}
}
/**
* Call load on our SegmentLoaders
*/
load() {
this.mainSegmentLoader_.load();
if (this.audioPlaylistLoader_) {
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
this.audioSegmentLoader_.load();
}
if (this.subtitlePlaylistLoader_) {
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
this.subtitleSegmentLoader_.load();
}
}
/**
* Returns the audio group for the currently active primary
* media playlist.
*/
activeAudioGroup() {
let videoPlaylist = this.masterPlaylistLoader_.media();
let result;
if (videoPlaylist.attributes.AUDIO) {
result = this.audioGroups_[videoPlaylist.attributes.AUDIO];
}
return result || this.audioGroups_.main;
}
/**
* Returns the subtitle group for the currently active primary
* media playlist.
*/
activeSubtitleGroup_() {
let videoPlaylist = this.masterPlaylistLoader_.media();
let result;
if (!videoPlaylist) {
return null;
}
if (videoPlaylist.attributes.SUBTITLES) {
result = this.subtitleGroups_.groups[videoPlaylist.attributes.SUBTITLES];
}
return result || this.subtitleGroups_.groups.main;
}
activeSubtitleTrack_() {
for (let trackName in this.subtitleGroups_.tracks) {
if (this.subtitleGroups_.tracks[trackName].mode === 'showing') {
return this.subtitleGroups_.tracks[trackName];
}
}
return null;
}
handleSubtitleError_() {
videojs.log.warn('Problem encountered loading the subtitle track' +
'. Switching back to default.');
this.subtitleSegmentLoader_.abort();
let track = this.activeSubtitleTrack_();
if (track) {
track.mode = 'disabled';
}
this.setupSubtitles();
}
/**
* Determine the correct audio renditions based on the active
* AudioTrack and initialize a PlaylistLoader and SegmentLoader if
* necessary. This method is only called when the media-group changes
* and performs non-destructive 'resync' of the SegmentLoader(s) since
* the playlist has likely changed
*/
mediaGroupChanged() {
let track = this.getActiveAudioTrack_();
this.stopAudioLoaders_();
this.resyncAudioLoaders_(track);
}
/**
* Determine the correct audio rendition based on the active
* AudioTrack and initialize a PlaylistLoader and SegmentLoader if
* necessary. This method is called once automatically before
* playback begins to enable the default audio track and should be
* invoked again if the track is changed. Performs destructive 'reset'
* on the SegmentLoaders(s) to ensure we start loading audio as
* close to currentTime as possible
*/
setupAudio() {
let track = this.getActiveAudioTrack_();
this.stopAudioLoaders_();
this.resetAudioLoaders_(track);
}
/**
* Returns the currently active track or the default track if none
* are active
*/
getActiveAudioTrack_() {
// determine whether seperate loaders are required for the audio
// rendition
let audioGroup = this.activeAudioGroup();
let track = audioGroup.filter((audioTrack) => {
return audioTrack.enabled;
})[0];
if (!track) {
track = audioGroup.filter((audioTrack) => {
return audioTrack.properties_.default;
})[0] || audioGroup[0];
track.enabled = true;
}
return track;
}
/**
* Destroy the PlaylistLoader and pause the SegmentLoader specifically
* for audio when switching audio tracks
*/
stopAudioLoaders_() {
// stop playlist and segment loading for audio
if (this.audioPlaylistLoader_) {
this.audioPlaylistLoader_.dispose();
this.audioPlaylistLoader_ = null;
}
this.audioSegmentLoader_.pause();
}
/**
* Destructive reset of the mainSegmentLoader (when audio is muxed)
* or audioSegmentLoader (when audio is demuxed) to prepare them
* to start loading new data right at currentTime
*/
resetAudioLoaders_(track) {
if (!track.properties_.resolvedUri) {
this.mainSegmentLoader_.resetEverything();
return;
}
this.audioSegmentLoader_.resetEverything();
this.setupAudioPlaylistLoader_(track);
}
/**
* Non-destructive resync of the audioSegmentLoader (when audio
* is demuxed) to prepare to continue appending new audio data
* at the end of the current buffered region
*/
resyncAudioLoaders_(track) {
if (!track.properties_.resolvedUri) {
return;
}
this.audioSegmentLoader_.resyncLoader();
this.setupAudioPlaylistLoader_(track);
}
/**
* Setup a new audioPlaylistLoader and start the audioSegmentLoader
* to begin loading demuxed audio
*/
setupAudioPlaylistLoader_(track) {
// startup playlist and segment loaders for the enabled audio
// track
this.audioPlaylistLoader_ = new PlaylistLoader(track.properties_.resolvedUri,
this.hls_,
this.withCredentials);
this.audioPlaylistLoader_.load();
this.audioPlaylistLoader_.on('loadedmetadata', () => {
let audioPlaylist = this.audioPlaylistLoader_.media();
this.audioSegmentLoader_.playlist(audioPlaylist, this.requestOptions_);
// if the video is already playing, or if this isn't a live video and preload
// permits, start downloading segments
if (!this.tech_.paused() ||
(audioPlaylist.endList && this.tech_.preload() !== 'none')) {
this.audioSegmentLoader_.load();
}
if (!audioPlaylist.endList) {
this.audioPlaylistLoader_.trigger('firstplay');
}
});
this.audioPlaylistLoader_.on('loadedplaylist', () => {
let updatedPlaylist;
if (this.audioPlaylistLoader_) {
updatedPlaylist = this.audioPlaylistLoader_.media();
}
if (!updatedPlaylist) {
// only one playlist to select
this.audioPlaylistLoader_.media(
this.audioPlaylistLoader_.playlists.master.playlists[0]);
return;
}
this.audioSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
});
this.audioPlaylistLoader_.on('error', () => {
videojs.log.warn('Problem encountered loading the alternate audio track' +
'. Switching back to default.');
this.audioSegmentLoader_.abort();
this.setupAudio();
});
}
/**
* Determine the correct subtitle playlist based on the active
* SubtitleTrack and initialize a PlaylistLoader and SegmentLoader if
* necessary. This method is called once automatically before
* playback begins to enable the default subtitle track and should be
* invoked again if the track is changed.
*/
setupSubtitles() {
let subtitleGroup = this.activeSubtitleGroup_();
let track = this.activeSubtitleTrack_();
this.subtitleSegmentLoader_.pause();
if (!track) {
// stop playlist and segment loading for subtitles
if (this.subtitlePlaylistLoader_) {
this.subtitlePlaylistLoader_.dispose();
this.subtitlePlaylistLoader_ = null;
}
return;
}
let properties = subtitleGroup.filter((subtitleProperties) => {
return subtitleProperties.id === track.id;
})[0];
// startup playlist and segment loaders for the enabled subtitle track
if (!this.subtitlePlaylistLoader_ ||
// if the media hasn't loaded yet, we don't have the URI to check, so it is
// easiest to simply recreate the playlist loader
!this.subtitlePlaylistLoader_.media() ||
this.subtitlePlaylistLoader_.media().resolvedUri !== properties.resolvedUri) {
if (this.subtitlePlaylistLoader_) {
this.subtitlePlaylistLoader_.dispose();
}
// reset the segment loader only when the subtitle playlist is changed instead of
// every time setupSubtitles is called since switching subtitle tracks fires
// multiple `change` events on the TextTrackList
this.subtitleSegmentLoader_.resetEverything();
// can't reuse playlistloader because we're only using single renditions and not a
// proper master
this.subtitlePlaylistLoader_ = new PlaylistLoader(properties.resolvedUri,
this.hls_,
this.withCredentials);
this.subtitlePlaylistLoader_.on('loadedmetadata', () => {
let subtitlePlaylist = this.subtitlePlaylistLoader_.media();
this.subtitleSegmentLoader_.playlist(subtitlePlaylist, this.requestOptions_);
this.subtitleSegmentLoader_.track(this.activeSubtitleTrack_());
// if the video is already playing, or if this isn't a live video and preload
// permits, start downloading segments
if (!this.tech_.paused() ||
(subtitlePlaylist.endList && this.tech_.preload() !== 'none')) {
this.subtitleSegmentLoader_.load();
}
});
this.subtitlePlaylistLoader_.on('loadedplaylist', () => {
let updatedPlaylist;
if (this.subtitlePlaylistLoader_) {
updatedPlaylist = this.subtitlePlaylistLoader_.media();
}
if (!updatedPlaylist) {
return;
}
this.subtitleSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
});
this.subtitlePlaylistLoader_.on('error', this.handleSubtitleError_.bind(this));
}
if (this.subtitlePlaylistLoader_.media() &&
this.subtitlePlaylistLoader_.media().resolvedUri === properties.resolvedUri) {
this.subtitleSegmentLoader_.load();
} else {
this.subtitlePlaylistLoader_.load();
}
}
/**
* Re-tune playback quality level for the current player
* conditions. This method may perform destructive actions, like
@ -1280,7 +753,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
onEndOfStream() {
let isEndOfStream = this.mainSegmentLoader_.ended_;
if (this.audioPlaylistLoader_) {
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
// if the audio playlist loader exists, then alternate audio is active, so we need
// to wait for both the main and audio segment loaders to call endOfStream
isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
@ -1391,10 +864,10 @@ export class MasterPlaylistController extends videojs.EventTarget {
*/
pauseLoading() {
this.mainSegmentLoader_.pause();
if (this.audioPlaylistLoader_) {
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
this.audioSegmentLoader_.pause();
}
if (this.subtitlePlaylistLoader_) {
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
this.subtitleSegmentLoader_.pause();
}
}
@ -1435,11 +908,11 @@ export class MasterPlaylistController extends videojs.EventTarget {
// location
this.mainSegmentLoader_.resetEverything();
this.mainSegmentLoader_.abort();
if (this.audioPlaylistLoader_) {
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
this.audioSegmentLoader_.resetEverything();
this.audioSegmentLoader_.abort();
}
if (this.subtitlePlaylistLoader_) {
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
this.subtitleSegmentLoader_.resetEverything();
this.subtitleSegmentLoader_.abort();
}
@ -1501,8 +974,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
return;
}
if (this.audioPlaylistLoader_) {
media = this.audioPlaylistLoader_.media();
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
media = this.mediaTypes_.AUDIO.activePlaylistLoader.media();
expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
if (expired === null) {
@ -1574,12 +1047,18 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.masterPlaylistLoader_.dispose();
this.mainSegmentLoader_.dispose();
if (this.audioPlaylistLoader_) {
this.audioPlaylistLoader_.dispose();
}
if (this.subtitlePlaylistLoader_) {
this.subtitlePlaylistLoader_.dispose();
}
['AUDIO', 'SUBTITLES'].forEach((type) => {
const groups = this.mediaTypes_[type].groups;
for (let id in groups) {
groups[id].forEach((group) => {
if (group.playlistLoader) {
group.playlistLoader.dispose();
}
});
}
});
this.audioSegmentLoader_.dispose();
this.subtitleSegmentLoader_.dispose();
}

751
src/media-groups.js

@ -0,0 +1,751 @@
import videojs from 'video.js';
import PlaylistLoader from './playlist-loader';
const noop = () => {};
/**
* Convert the properties of an HLS track into an audioTrackKind.
*
* @private
*/
const audioTrackKind_ = (properties) => {
let kind = properties.default ? 'main' : 'alternative';
if (properties.characteristics &&
properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
kind = 'main-desc';
}
return kind;
};
/**
* Pause provided segment loader and playlist loader if active
*
* @param {SegmentLoader} segmentLoader
* SegmentLoader to pause
* @param {Object} mediaType
* Active media type
* @function stopLoaders
*/
export const stopLoaders = (segmentLoader, mediaType) => {
segmentLoader.abort();
segmentLoader.pause();
if (mediaType && mediaType.activePlaylistLoader) {
mediaType.activePlaylistLoader.pause();
mediaType.activePlaylistLoader = null;
}
};
/**
* Start loading provided segment loader and playlist loader
*
* @param {PlaylistLoader} playlistLoader
* PlaylistLoader to start loading
* @param {Object} mediaType
* Active media type
* @function startLoaders
*/
export const startLoaders = (playlistLoader, mediaType) => {
// Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the
// playlist loader
mediaType.activePlaylistLoader = playlistLoader;
playlistLoader.load();
};
/**
* Returns a function to be called when the media group changes. It performs a
* non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a
* change of group is merely a rendition switch of the same content at another encoding,
* rather than a change of content, such as switching audio from English to Spanish.
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @return {Function}
* Handler for a non-destructive resync of SegmentLoader when the active media
* group changes.
* @function onGroupChanged
*/
export const onGroupChanged = (type, settings) => () => {
const {
segmentLoaders: {
[type]: segmentLoader,
main: mainSegmentLoader
},
mediaTypes: { [type]: mediaType }
} = settings;
const activeTrack = mediaType.activeTrack();
const activeGroup = mediaType.activeGroup(activeTrack);
const previousActiveLoader = mediaType.activePlaylistLoader;
stopLoaders(segmentLoader, mediaType);
if (!activeGroup) {
// there is no group active
return;
}
if (!activeGroup.playlistLoader) {
if (previousActiveLoader) {
// The previous group had a playlist loader but the new active group does not
// this means we are switching from demuxed to muxed audio. In this case we want to
// do a destructive reset of the main segment loader and not restart the audio
// loaders.
mainSegmentLoader.resetEverything();
}
return;
}
// Non-destructive resync
segmentLoader.resyncLoader();
startLoaders(activeGroup.playlistLoader, mediaType);
};
/**
* Returns a function to be called when the media track changes. It performs a
* destructive reset of the SegmentLoader to ensure we start loading as close to
* currentTime as possible.
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @return {Function}
* Handler for a destructive reset of SegmentLoader when the active media
* track changes.
* @function onTrackChanged
*/
export const onTrackChanged = (type, settings) => () => {
const {
segmentLoaders: {
[type]: segmentLoader,
main: mainSegmentLoader
},
mediaTypes: { [type]: mediaType }
} = settings;
const activeTrack = mediaType.activeTrack();
const activeGroup = mediaType.activeGroup(activeTrack);
const previousActiveLoader = mediaType.activePlaylistLoader;
stopLoaders(segmentLoader, mediaType);
if (!activeGroup) {
// there is no group active so we do not want to restart loaders
return;
}
if (!activeGroup.playlistLoader) {
// when switching from demuxed audio/video to muxed audio/video (noted by no playlist
// loader for the audio group), we want to do a destructive reset of the main segment
// loader and not restart the audio loaders
mainSegmentLoader.resetEverything();
return;
}
if (previousActiveLoader === activeGroup.playlistLoader) {
// Nothing has actually changed. This can happen because track change events can fire
// multiple times for a "single" change. One for enabling the new active track, and
// one for disabling the track that was active
startLoaders(activeGroup.playlistLoader, mediaType);
return;
}
if (segmentLoader.track) {
// For WebVTT, set the new text track in the segmentloader
segmentLoader.track(activeTrack);
}
// destructive reset
segmentLoader.resetEverything();
startLoaders(activeGroup.playlistLoader, mediaType);
};
export const onError = {
/**
* Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
* an error.
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @return {Function}
* Error handler. Logs warning (or error if the playlist is blacklisted) to
* console and switches back to default audio track.
* @function onError.AUDIO
*/
AUDIO: (type, settings) => () => {
const {
segmentLoaders: { [type]: segmentLoader},
mediaTypes: { [type]: mediaType },
blacklistCurrentPlaylist
} = settings;
stopLoaders(segmentLoader, mediaType);
// switch back to default audio track
const activeTrack = mediaType.activeTrack();
const activeGroup = mediaType.activeGroup();
const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id;
const defaultTrack = mediaType.tracks[id];
if (activeTrack === defaultTrack) {
// Default track encountered an error. All we can do now is blacklist the current
// rendition and hope another will switch audio groups
blacklistCurrentPlaylist({
message: 'Problem encountered loading the default audio track.'
});
return;
}
videojs.log.warn('Problem encountered loading the alternate audio track.' +
'Switching back to default.');
for (let trackId in mediaType.tracks) {
mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack;
}
mediaType.onTrackChanged();
},
/**
* Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
* an error.
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @return {Function}
* Error handler. Logs warning to console and disables the active subtitle track
* @function onError.SUBTITLES
*/
SUBTITLES: (type, settings) => () => {
const {
segmentLoaders: { [type]: segmentLoader},
mediaTypes: { [type]: mediaType }
} = settings;
videojs.log.warn('Problem encountered loading the subtitle track.' +
'Disabling subtitle track.');
stopLoaders(segmentLoader, mediaType);
const track = mediaType.activeTrack();
if (track) {
track.mode = 'disabled';
}
mediaType.onTrackChanged();
}
};
export const setupListeners = {
/**
* Setup event listeners for audio playlist loader
*
* @param {String} type
* MediaGroup type
* @param {PlaylistLoader|null} playlistLoader
* PlaylistLoader to register listeners on
* @param {Object} settings
* Object containing required information for media groups
* @function setupListeners.AUDIO
*/
AUDIO: (type, playlistLoader, settings) => {
if (!playlistLoader) {
// no playlist loader means audio will be muxed with the video
return;
}
const {
tech,
requestOptions,
segmentLoaders: { [type]: segmentLoader }
} = settings;
playlistLoader.on('loadedmetadata', () => {
const media = playlistLoader.media();
segmentLoader.playlist(media, requestOptions);
// if the video is already playing, or if this isn't a live video and preload
// permits, start downloading segments
if (!tech.paused() || (media.endList && tech.preload() !== 'none')) {
segmentLoader.load();
}
});
playlistLoader.on('loadedplaylist', () => {
segmentLoader.playlist(playlistLoader.media(), requestOptions);
// If the player isn't paused, ensure that the segment loader is running
if (!tech.paused()) {
segmentLoader.load();
}
});
playlistLoader.on('error', onError[type](type, settings));
},
/**
* Setup event listeners for subtitle playlist loader
*
* @param {String} type
* MediaGroup type
* @param {PlaylistLoader|null} playlistLoader
* PlaylistLoader to register listeners on
* @param {Object} settings
* Object containing required information for media groups
* @function setupListeners.SUBTITLES
*/
SUBTITLES: (type, playlistLoader, settings) => {
const {
tech,
requestOptions,
segmentLoaders: { [type]: segmentLoader },
mediaTypes: { [type]: mediaType }
} = settings;
playlistLoader.on('loadedmetadata', () => {
const media = playlistLoader.media();
segmentLoader.playlist(media, requestOptions);
segmentLoader.track(mediaType.activeTrack());
// if the video is already playing, or if this isn't a live video and preload
// permits, start downloading segments
if (!tech.paused() || (media.endList && tech.preload() !== 'none')) {
segmentLoader.load();
}
});
playlistLoader.on('loadedplaylist', () => {
segmentLoader.playlist(playlistLoader.media(), requestOptions);
// If the player isn't paused, ensure that the segment loader is running
if (!tech.paused()) {
segmentLoader.load();
}
});
playlistLoader.on('error', onError[type](type, settings));
}
};
export const initialize = {
/**
* Setup PlaylistLoaders and AudioTracks for the audio groups
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @function initialize.AUDIO
*/
'AUDIO': (type, settings) => {
const {
mode,
hls,
segmentLoaders: { [type]: segmentLoader },
requestOptions: { withCredentials },
master: { mediaGroups },
mediaTypes: {
[type]: {
groups,
tracks
}
}
} = settings;
// force a default if we have none or we are not
// in html5 mode (the only mode to support more than one
// audio track)
if (!mediaGroups[type] ||
Object.keys(mediaGroups[type]).length === 0 ||
mode !== 'html5') {
mediaGroups[type] = { main: { default: { default: true } } };
}
for (let groupId in mediaGroups[type]) {
if (!groups[groupId]) {
groups[groupId] = [];
}
for (let variantLabel in mediaGroups[type][groupId]) {
let properties = mediaGroups[type][groupId][variantLabel];
let playlistLoader;
if (properties.resolvedUri) {
playlistLoader = new PlaylistLoader(properties.resolvedUri,
hls,
withCredentials);
} else {
// no resolvedUri means the audio is muxed with the video when using this
// audio track
playlistLoader = null;
}
properties = videojs.mergeOptions({ id: variantLabel, playlistLoader },
properties);
setupListeners[type](type, properties.playlistLoader, settings);
groups[groupId].push(properties);
if (typeof tracks[variantLabel] === 'undefined') {
const track = new videojs.AudioTrack({
id: variantLabel,
kind: audioTrackKind_(properties),
enabled: false,
language: properties.language,
default: properties.default,
label: variantLabel
});
tracks[variantLabel] = track;
}
}
}
// setup single error event handler for the segment loader
segmentLoader.on('error', onError[type](type, settings));
},
/**
* Setup PlaylistLoaders and TextTracks for the subtitle groups
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @function initialize.SUBTITLES
*/
'SUBTITLES': (type, settings) => {
const {
tech,
hls,
segmentLoaders: { [type]: segmentLoader },
requestOptions: { withCredentials },
master: { mediaGroups },
mediaTypes: {
[type]: {
groups,
tracks
}
}
} = settings;
for (let groupId in mediaGroups[type]) {
if (!groups[groupId]) {
groups[groupId] = [];
}
for (let variantLabel in mediaGroups[type][groupId]) {
if (mediaGroups[type][groupId][variantLabel].forced) {
// Subtitle playlists with the forced attribute are not selectable in Safari.
// According to Apple's HLS Authoring Specification:
// If content has forced subtitles and regular subtitles in a given language,
// the regular subtitles track in that language MUST contain both the forced
// subtitles and the regular subtitles for that language.
// Because of this requirement and that Safari does not add forced subtitles,
// forced subtitles are skipped here to maintain consistent experience across
// all platforms
continue;
}
let properties = mediaGroups[type][groupId][variantLabel];
properties = videojs.mergeOptions({
id: variantLabel,
playlistLoader: new PlaylistLoader(properties.resolvedUri,
hls,
withCredentials)
}, properties);
setupListeners[type](type, properties.playlistLoader, settings);
groups[groupId].push(properties);
if (typeof tracks[variantLabel] === 'undefined') {
const track = tech.addRemoteTextTrack({
id: variantLabel,
kind: 'subtitles',
enabled: false,
language: properties.language,
label: variantLabel
}, false).track;
tracks[variantLabel] = track;
}
}
}
// setup single error event handler for the segment loader
segmentLoader.on('error', onError[type](type, settings));
},
/**
* Setup TextTracks for the closed-caption groups
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @function initialize['CLOSED-CAPTIONS']
*/
'CLOSED-CAPTIONS': (type, settings) => {
const {
tech,
master: { mediaGroups },
mediaTypes: {
[type]: {
groups,
tracks
}
}
} = settings;
for (let groupId in mediaGroups[type]) {
if (!groups[groupId]) {
groups[groupId] = [];
}
for (let variantLabel in mediaGroups[type][groupId]) {
let properties = mediaGroups[type][groupId][variantLabel];
// We only support CEA608 captions for now, so ignore anything that
// doesn't use a CCx INSTREAM-ID
if (!properties.instreamId.match(/CC\d/)) {
continue;
}
// No PlaylistLoader is required for Closed-Captions because the captions are
// embedded within the video stream
groups[groupId].push(videojs.mergeOptions({ id: variantLabel }, properties));
if (typeof tracks[variantLabel] === 'undefined') {
const track = tech.addRemoteTextTrack({
id: properties.instreamId,
kind: 'captions',
enabled: false,
language: properties.language,
label: variantLabel
}, false).track;
tracks[variantLabel] = track;
}
}
}
}
};
/**
* Returns a function used to get the active group of the provided type
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @return {Function}
* Function that returns the active media group for the provided type. Takes an
* optional parameter {TextTrack} track. If no track is provided, a list of all
* variants in the group, otherwise the variant corresponding to the provided
* track is returned.
* @function activeGroup
*/
export const activeGroup = (type, settings) => (track) => {
const {
masterPlaylistLoader,
mediaTypes: { [type]: { groups } }
} = settings;
const media = masterPlaylistLoader.media();
if (!media) {
return null;
}
let variants = null;
if (media.attributes[type]) {
variants = groups[media.attributes[type]];
}
variants = variants || groups.main;
if (typeof track === 'undefined') {
return variants;
}
if (track === null) {
// 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;
}
return variants.filter((props) => props.id === track.id)[0] || null;
};
export const activeTrack = {
/**
* Returns a function used to get the active track of type provided
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @return {Function}
* Function that returns the active media track for the provided type. Returns
* null if no track is active
* @function activeTrack.AUDIO
*/
AUDIO: (type, settings) => () => {
const { mediaTypes: { [type]: { tracks } } } = settings;
for (let id in tracks) {
if (tracks[id].enabled) {
return tracks[id];
}
}
return null;
},
/**
* Returns a function used to get the active track of type provided
*
* @param {String} type
* MediaGroup type
* @param {Object} settings
* Object containing required information for media groups
* @return {Function}
* Function that returns the active media track for the provided type. Returns
* null if no track is active
* @function activeTrack.SUBTITLES
*/
SUBTITLES: (type, settings) => () => {
const { mediaTypes: { [type]: { tracks } } } = settings;
for (let id in tracks) {
if (tracks[id].mode === 'showing') {
return tracks[id];
}
}
return null;
}
};
/**
* Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles,
* Closed-Captions) specified in the master manifest.
*
* @param {Object} settings
* Object containing required information for setting up the media groups
* @param {SegmentLoader} settings.segmentLoaders.AUDIO
* Audio segment loader
* @param {SegmentLoader} settings.segmentLoaders.SUBTITLES
* Subtitle segment loader
* @param {SegmentLoader} settings.segmentLoaders.main
* Main segment loader
* @param {Tech} settings.tech
* The tech of the player
* @param {Object} settings.requestOptions
* XHR request options used by the segment loaders
* @param {PlaylistLoader} settings.masterPlaylistLoader
* PlaylistLoader for the master source
* @param {String} mode
* Mode of the hls source handler. Can be 'auto', 'html5', or 'flash'
* @param {HlsHandler} settings.hls
* HLS SourceHandler
* @param {Object} settings.master
* The parsed master manifest
* @param {Object} settings.mediaTypes
* Object to store the loaders, tracks, and utility methods for each media type
* @param {Function} settings.blacklistCurrentPlaylist
* Blacklists the current rendition and forces a rendition switch.
* @function setupMediaGroups
*/
export const setupMediaGroups = (settings) => {
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => {
initialize[type](type, settings);
});
const {
mediaTypes,
masterPlaylistLoader,
tech,
hls
} = settings;
// setup active group and track getters and change event handlers
['AUDIO', 'SUBTITLES'].forEach((type) => {
mediaTypes[type].activeGroup = activeGroup(type, settings);
mediaTypes[type].activeTrack = activeTrack[type](type, settings);
mediaTypes[type].onGroupChanged = onGroupChanged(type, settings);
mediaTypes[type].onTrackChanged = onTrackChanged(type, settings);
});
// DO NOT enable the default subtitle or caption track.
// DO enable the default audio track
const audioGroup = mediaTypes.AUDIO.activeGroup();
const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id;
mediaTypes.AUDIO.tracks[groupId].enabled = true;
mediaTypes.AUDIO.onTrackChanged();
masterPlaylistLoader.on('mediachange', () => {
['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged());
});
// custom audio track change event handler for usage event
const onAudioTrackChanged = () => {
mediaTypes.AUDIO.onTrackChanged();
tech.trigger({ type: 'usage', name: 'hls-audio-change' });
};
tech.audioTracks().addEventListener('change', onAudioTrackChanged);
tech.remoteTextTracks().addEventListener('change',
mediaTypes.SUBTITLES.onTrackChanged);
hls.on('dispose', () => {
tech.audioTracks().removeEventListener('change', onAudioTrackChanged);
tech.remoteTextTracks().removeEventListener('change',
mediaTypes.SUBTITLES.onTrackChanged);
});
// clear existing audio tracks and add the ones we just created
tech.clearTracks('audio');
for (let id in mediaTypes.AUDIO.tracks) {
tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]);
}
};
/**
* Creates skeleton object used to store the loaders, tracks, and utility methods for each
* media type
*
* @return {Object}
* Object to store the loaders, tracks, and utility methods for each media type
* @function createMediaTypes
*/
export const createMediaTypes = () => {
const mediaTypes = {};
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => {
mediaTypes[type] = {
groups: {},
tracks: {},
activePlaylistLoader: null,
activeGroup: noop,
activeTrack: noop,
onGroupChanged: noop,
onTrackChanged: noop
};
});
return mediaTypes;
};

28
src/playlist-loader.js

@ -346,6 +346,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
if (this.media_) {
this.trigger('mediachanging');
}
request = this.hls_.xhr({
uri: resolveUrl(loader.master.uri, playlist.uri),
withCredentials
@ -385,6 +386,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
}
loader.state = 'HAVE_CURRENT_METADATA';
request = this.hls_.xhr({
uri: resolveUrl(loader.master.uri, loader.media().uri),
withCredentials
@ -397,22 +399,11 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
if (error) {
return playlistRequestError(request, loader.media().uri, 'HAVE_METADATA');
}
haveMetadata(request, loader.media().uri);
});
});
// setup initial sync info
loader.on('firstplay', function() {
let playlist = loader.media();
if (playlist) {
playlist.syncInfo = {
mediaSequence: playlist.mediaSequence,
time: 0
};
}
});
/**
* pause loading of the playlist
*/
@ -424,6 +415,19 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
// started, so reset to an unstarted state.
loader.started = false;
}
// Need to restore state now that no activity is happening
if (loader.state === 'SWITCHING_MEDIA') {
// if the loader was in the process of switching media, it should either return to
// HAVE_MASTER or HAVE_METADATA depending on if the loader has loaded a media
// playlist yet. This is determined by the existence of loader.media_
if (loader.media_) {
loader.state = 'HAVE_METADATA';
} else {
loader.state = 'HAVE_MASTER';
}
} else if (loader.state === 'HAVE_CURRENT_METADATA') {
loader.state = 'HAVE_METADATA';
}
};
/**

12
src/sync-controller.js

@ -358,7 +358,8 @@ export default class SyncController extends videojs.EventTarget {
* @param {SegmentInfo} segmentInfo - The current active request information
*/
probeSegmentInfo(segmentInfo) {
let segment = segmentInfo.segment;
const segment = segmentInfo.segment;
const playlist = segmentInfo.playlist;
let timingInfo;
if (segment.map) {
@ -370,6 +371,15 @@ export default class SyncController extends videojs.EventTarget {
if (timingInfo) {
if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) {
this.saveDiscontinuitySyncInfo_(segmentInfo);
// If the playlist does not have sync information yet, record that information
// now with segment timing information
if (!playlist.syncInfo) {
playlist.syncInfo = {
mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
time: segment.start
};
}
}
}
}

53
src/videojs-contrib-hls.js

@ -160,26 +160,6 @@ Hls.isSupported = function() {
'your player\'s techOrder.');
};
const USER_AGENT = window.navigator && window.navigator.userAgent || '';
/**
* Determines whether the browser supports a change in the audio configuration
* during playback. Currently only Firefox 48 and below do not support this.
* window.isSecureContext is a propterty that was added to window in firefox 49,
* so we can use it to detect Firefox 49+.
*
* @return {Boolean} Whether the browser supports audio config change during playback
*/
Hls.supportsAudioInfoChange_ = function() {
if (videojs.browser.IS_FIREFOX) {
let firefoxVersionMap = (/Firefox\/([\d.]+)/i).exec(USER_AGENT);
let version = parseInt(firefoxVersionMap[1], 10);
return version >= 49;
}
return true;
};
const Component = videojs.getComponent('Component');
/**
@ -256,15 +236,6 @@ class HlsHandler extends Component {
}
});
this.audioTrackChange_ = () => {
this.masterPlaylistController_.setupAudio();
this.tech_.trigger({type: 'usage', name: 'hls-audio-change'});
};
this.textTrackChange_ = () => {
this.masterPlaylistController_.setupSubtitles();
};
this.on(this.tech_, 'play', this.play);
}
@ -444,24 +415,11 @@ class HlsHandler extends Component {
this.tech_.one('canplay',
this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_));
this.masterPlaylistController_.on('sourceopen', () => {
this.tech_.audioTracks().addEventListener('change', this.audioTrackChange_);
this.tech_.remoteTextTracks().addEventListener('change', this.textTrackChange_);
});
this.masterPlaylistController_.on('selectedinitialmedia', () => {
// Add the manual rendition mix-in to HlsHandler
renditionSelectionMixin(this);
});
this.masterPlaylistController_.on('audioupdate', () => {
// clear current audioTracks
this.tech_.clearTracks('audio');
this.masterPlaylistController_.activeAudioGroup().forEach((audioTrack) => {
this.tech_.audioTracks().addTrack(audioTrack);
});
});
// the bandwidth of the primary segment loader is our best
// estimate of overall bandwidth
this.on(this.masterPlaylistController_, 'progress', function() {
@ -508,15 +466,6 @@ class HlsHandler extends Component {
}
}
/**
* a helper for grabbing the active audio group from MasterPlaylistController
*
* @private
*/
activeAudioGroup_() {
return this.masterPlaylistController_.activeAudioGroup();
}
/**
* Begin playing the video.
*/
@ -558,8 +507,6 @@ class HlsHandler extends Component {
if (this.qualityLevels_) {
this.qualityLevels_.dispose();
}
this.tech_.audioTracks().removeEventListener('change', this.audioTrackChange_);
this.tech_.remoteTextTracks().removeEventListener('change', this.textTrackChange_);
super.dispose();
}
}

12
src/vtt-segment-loader.js

@ -118,10 +118,16 @@ export default class VTTSegmentLoader extends SegmentLoader {
/**
* Set a subtitle track on the segment loader to add subtitles to
*
* @param {TextTrack} track
* @param {TextTrack=} track
* The text track to add loaded subtitles to
* @return {TextTrack}
* Returns the subtitles track
*/
track(track) {
if (typeof track === 'undefined') {
return this.subtitlesTrack_;
}
this.subtitlesTrack_ = track;
// if we were unpaused but waiting for a sourceUpdater, start
@ -129,6 +135,8 @@ export default class VTTSegmentLoader extends SegmentLoader {
if (this.state === 'INIT' && this.couldBeginLoading_()) {
this.init_();
}
return this.subtitlesTrack_;
}
/**
@ -217,7 +225,7 @@ export default class VTTSegmentLoader extends SegmentLoader {
* @private
*/
handleSegment_() {
if (!this.pendingSegment_) {
if (!this.pendingSegment_ || !this.subtitlesTrack_) {
this.state = 'READY';
return;
}

299
test/master-playlist-controller.test.js

@ -546,7 +546,8 @@ function(assert) {
'1024 bytes downloaded');
});
QUnit.test('updates the enabled track when switching audio groups', function(assert) {
QUnit.test('updates the active loader when switching from unmuxed to muxed audio group',
function(assert) {
openMediaSource(this.player, this.clock);
// master
this.requests.shift().respond(200, null,
@ -565,6 +566,9 @@ QUnit.test('updates the enabled track when switching audio groups', function(ass
let mpc = this.masterPlaylistController;
let combinedPlaylist = mpc.master().playlists[0];
assert.ok(mpc.mediaTypes_.AUDIO.activePlaylistLoader,
'starts with an active playlist loader');
mpc.masterPlaylistLoader_.media(combinedPlaylist);
// updated media
this.requests.shift().respond(200, null,
@ -573,8 +577,8 @@ QUnit.test('updates the enabled track when switching audio groups', function(ass
'0.ts\n' +
'#EXT-X-ENDLIST\n');
assert.ok(mpc.activeAudioGroup().filter((track) => track.enabled)[0],
'enabled a track in the new audio group');
assert.notOk(mpc.mediaTypes_.AUDIO.activePlaylistLoader,
'enabled a track in the new audio group');
});
QUnit.test('waits for both main and audio loaders to finish before calling endOfStream',
@ -863,40 +867,6 @@ function(assert) {
assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
});
QUnit.test('blacklists the current playlist when audio changes in Firefox 48 & below',
function(assert) {
videojs.browser.IS_FIREFOX = true;
let origSupportsAudioInfoChange_ = videojs.Hls.supportsAudioInfoChange_;
videojs.Hls.supportsAudioInfoChange_ = () => false;
// master
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());
let media = this.masterPlaylistController.media();
// initial audio config
this.masterPlaylistController.mediaSource.trigger({
type: 'audioinfo',
info: {}
});
// updated audio config
this.masterPlaylistController.mediaSource.trigger({
type: 'audioinfo',
info: {
different: true
}
});
assert.ok(media.excludeUntil > 0, 'blacklisted the old playlist');
assert.equal(this.env.log.warn.callCount, 2, 'logged two warnings');
this.env.log.warn.callCount = 0;
videojs.Hls.supportsAudioInfoChange_ = origSupportsAudioInfoChange_;
});
QUnit.test('updates the combined segment loader on media changes', function(assert) {
let updates = [];
@ -1563,7 +1533,7 @@ function(assert) {
videojs.createTimeRanges([[0, 10]]),
'main when no audio');
mpc.audioPlaylistLoader_ = {
mpc.mediaTypes_.AUDIO.activePlaylistLoader = {
media: () => audioMedia,
dispose() {},
expired_: 0
@ -1903,7 +1873,8 @@ QUnit.test('trigger events when an fMP4 stream is detected', function(assert) {
Hls.Playlist.isFmp4 = isFmp4Copy;
});
QUnit.test('adds only CEA608 closed-caption tracks when a master playlist is loaded', function(assert) {
QUnit.test('adds only CEA608 closed-caption tracks when a master playlist is loaded',
function(assert) {
this.requests.length = 0;
this.player = createPlayer();
this.player.src({
@ -1934,7 +1905,7 @@ QUnit.test('adds only CEA608 closed-caption tracks when a master playlist is loa
const master = masterPlaylistController.masterPlaylistLoader_.master;
const caps = master.mediaGroups['CLOSED-CAPTIONS'].CCs;
const capsArr = Object.keys(caps).map(key => Object.assign({name: key}, caps[key]));
const addedCaps = masterPlaylistController.closedCaptionGroups_.groups.CCs
const addedCaps = masterPlaylistController.mediaTypes_['CLOSED-CAPTIONS'].groups.CCs
.map(cap => Object.assign({name: cap.id}, cap));
assert.equal(capsArr.length, 4, '4 closed-caption tracks defined in playlist');
@ -1947,10 +1918,14 @@ QUnit.test('adds only CEA608 closed-caption tracks when a master playlist is loa
assert.equal(textTracks.length, 3, '2 text tracks were added');
assert.equal(textTracks[1].mode, 'disabled', 'track starts disabled');
assert.equal(textTracks[2].mode, 'disabled', 'track starts disabled');
assert.equal(textTracks[1].id, addedCaps[0].instreamId, 'text track 1\'s id is CC\'s instreamId');
assert.equal(textTracks[2].id, addedCaps[1].instreamId, 'text track 2\'s id is CC\'s instreamId');
assert.equal(textTracks[1].label, addedCaps[0].name, 'text track 1\'s label is CC\'s name');
assert.equal(textTracks[2].label, addedCaps[1].name, 'text track 2\'s label is CC\'s name');
assert.equal(textTracks[1].id, addedCaps[0].instreamId,
'text track 1\'s id is CC\'s instreamId');
assert.equal(textTracks[2].id, addedCaps[1].instreamId,
'text track 2\'s id is CC\'s instreamId');
assert.equal(textTracks[1].label, addedCaps[0].name,
'text track 1\'s label is CC\'s name');
assert.equal(textTracks[2].label, addedCaps[1].name,
'text track 2\'s label is CC\'s name');
});
QUnit.test('adds subtitle tracks when a media playlist is loaded', function(assert) {
@ -2139,7 +2114,7 @@ QUnit.test('disposes subtitle loaders on dispose', function(assert) {
let masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
assert.notOk(masterPlaylistController.mediaTypes_.SUBTITLES.activePlaylistLoader,
'does not start with a subtitle playlist loader');
assert.ok(masterPlaylistController.subtitleSegmentLoader_,
'starts with a subtitle segment loader');
@ -2179,7 +2154,7 @@ QUnit.test('disposes subtitle loaders on dispose', function(assert) {
assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
textTracks[1].mode = 'showing';
assert.ok(masterPlaylistController.subtitlePlaylistLoader_,
assert.ok(masterPlaylistController.mediaTypes_.SUBTITLES.activePlaylistLoader,
'has a subtitle playlist loader');
assert.ok(masterPlaylistController.subtitleSegmentLoader_,
'has a subtitle segment loader');
@ -2188,7 +2163,7 @@ QUnit.test('disposes subtitle loaders on dispose', function(assert) {
segmentLoaderDisposeCount = 0;
masterPlaylistController.subtitlePlaylistLoader_.dispose =
masterPlaylistController.mediaTypes_.SUBTITLES.activePlaylistLoader.dispose =
() => playlistLoaderDisposeCount++;
masterPlaylistController.subtitleSegmentLoader_.dispose =
() => segmentLoaderDisposeCount++;
@ -2252,236 +2227,6 @@ QUnit.test('subtitle segment loader resets on seeks', function(assert) {
assert.equal(loadCount, 1, 'called load on subtitle segment loader');
});
QUnit.test('can get active subtitle group', function(assert) {
this.requests.length = 0;
this.player = createPlayer();
this.player.src({
src: 'manifest/master-subtitles.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
assert.notOk(masterPlaylistController.activeSubtitleGroup_(),
'no active subtitle group');
// master, contains media groups for subtitles
this.standardXHRResponse(this.requests.shift());
assert.notOk(masterPlaylistController.activeSubtitleGroup_(),
'no active subtitle group');
// media
this.standardXHRResponse(this.requests.shift());
assert.ok(masterPlaylistController.activeSubtitleGroup_(), 'active subtitle group');
});
QUnit.test('can get active subtitle track', function(assert) {
this.requests.length = 0;
this.player = createPlayer();
this.player.src({
src: 'manifest/master-subtitles.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
// master, contains media groups for subtitles
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());
const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
assert.notOk(masterPlaylistController.activeSubtitleTrack_(),
'no active subtitle track');
const textTracks = this.player.textTracks();
// enable first subtitle text track
assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
textTracks[1].mode = 'showing';
assert.ok(masterPlaylistController.activeSubtitleTrack_(), 'active subtitle track');
});
QUnit.test('handles subtitle errors appropriately', function(assert) {
this.requests.length = 0;
this.player = createPlayer();
this.player.src({
src: 'manifest/master-subtitles.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
// master, contains media groups for subtitles
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());
const textTracks = this.player.textTracks();
// enable first subtitle text track
assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
textTracks[1].mode = 'showing';
const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
let abortCalls = 0;
let setupSubtitlesCalls = 0;
masterPlaylistController.subtitleSegmentLoader_.abort = () => abortCalls++;
masterPlaylistController.setupSubtitles = () => setupSubtitlesCalls++;
masterPlaylistController.handleSubtitleError_();
assert.equal(textTracks[1].mode, 'disabled', 'set text track to disabled');
assert.equal(abortCalls, 1, 'aborted subtitle segment loader');
assert.equal(setupSubtitlesCalls, 1, 'setup subtitles');
assert.equal(this.env.log.warn.callCount, 1, 'logged a warning');
this.env.log.warn.callCount = 0;
});
QUnit.test('sets up subtitles', function(assert) {
this.requests.length = 0;
this.player = createPlayer();
this.player.src({
src: 'manifest/master-subtitles.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
// master, contains media groups for subtitles
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());
const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
// sets up listener for text track changes
masterPlaylistController.trigger('sourceopen');
const segmentLoader = masterPlaylistController.subtitleSegmentLoader_;
let segmentDisposeCalls = 0;
let segmentLoadCalls = 0;
let segmentPauseCalls = 0;
let segmentResetCalls = 0;
segmentLoader.load = () => segmentLoadCalls++;
segmentLoader.dispose = () => segmentDisposeCalls++;
segmentLoader.pause = () => segmentPauseCalls++;
segmentLoader.resetEverything = () => segmentResetCalls++;
assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
'no subtitle playlist loader');
// no active text track
masterPlaylistController.setupSubtitles();
assert.equal(segmentDisposeCalls, 0, 'did not dispose subtitles segment loader');
assert.equal(segmentLoadCalls, 0, 'did not load subtitles segment loader');
assert.equal(segmentPauseCalls, 1, 'paused subtitles segment loader');
assert.equal(segmentResetCalls, 0, 'did not reset subtitle segment loader');
assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
'no subtitle playlist loader');
assert.ok(masterPlaylistController.subtitleSegmentLoader_,
'did not remove subtitle segment loader');
const textTracks = this.player.textTracks();
// enable first subtitle text track
assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
textTracks[1].mode = 'showing';
assert.ok(masterPlaylistController.subtitlePlaylistLoader_,
'added a new subtitle playlist loader');
assert.equal(segmentLoader,
masterPlaylistController.subtitleSegmentLoader_,
'did not change subtitle segment loader');
assert.equal(segmentLoadCalls, 0, 'did not load subtitles segment loader');
assert.equal(segmentResetCalls, 1, 'reset subtitle segment loader');
let playlistLoader = masterPlaylistController.subtitlePlaylistLoader_;
let playlistLoadCalls = 0;
playlistLoader.load = () => playlistLoadCalls++;
// same active text track, haven't yet gotten a response from webvtt
masterPlaylistController.setupSubtitles();
assert.equal(this.requests.length, 2, 'total of two requests');
let oldRequest = this.requests.shift();
// tracking playlist loader dispose calls by checking request aborted status
assert.ok(oldRequest.aborted, 'aborted the old request');
assert.notEqual(playlistLoader,
masterPlaylistController.subtitlePlaylistLoader_,
'changed subtitle playlist loader');
let playlistDisposeCalls = 0;
playlistLoader = masterPlaylistController.subtitlePlaylistLoader_;
playlistLoadCalls = 0;
playlistLoader.load = () => playlistLoadCalls++;
playlistLoader.dispose = () => playlistDisposeCalls++;
this.requests.shift().respond(200, null, `
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10
0.webvtt
#EXT-X-ENDLIST
`);
segmentLoadCalls = 0;
// same active text track, got a response from webvtt playlist
masterPlaylistController.setupSubtitles();
assert.equal(playlistLoader,
masterPlaylistController.subtitlePlaylistLoader_,
'did not change subtitle playlist loader');
assert.equal(segmentLoader,
masterPlaylistController.subtitleSegmentLoader_,
'did not change subtitle segment loader');
assert.equal(playlistDisposeCalls, 0, 'did not dispose subtitles playlist loader');
assert.equal(playlistLoadCalls, 0, 'did not load subtitles playlist loader');
assert.equal(segmentLoadCalls, 1, 'loaded subtitles segment loader');
playlistDisposeCalls = 0;
segmentDisposeCalls = 0;
playlistLoadCalls = 0;
segmentLoadCalls = 0;
segmentPauseCalls = 0;
segmentResetCalls = 0;
// turn off active subtitle text track
textTracks[1].mode = 'disabled';
assert.equal(playlistDisposeCalls, 1, 'disposed subtitles playlist loader');
assert.equal(segmentDisposeCalls, 0, 'did not dispose subtitles segment loader');
assert.equal(playlistLoadCalls, 0, 'did not load subtitles playlist loader');
assert.equal(segmentLoadCalls, 0, 'did not load subtitles segment loader');
assert.equal(segmentPauseCalls, 1, 'paused subtitles segment loader');
assert.equal(segmentResetCalls, 0, 'did not reset subtitle segment loader');
assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
'removed subtitle playlist loader');
assert.ok(masterPlaylistController.subtitleSegmentLoader_,
'did not remove subtitle segment loader');
});
QUnit.test('calculates dynamic GOAL_BUFFER_LENGTH', function(assert) {
const configOld = {
GOAL_BUFFER_LENGTH: Config.GOAL_BUFFER_LENGTH,

749
test/media-groups.test.js

@ -0,0 +1,749 @@
import QUnit from 'qunit';
import {
useFakeEnvironment
} from './test-helpers.js';
import * as MediaGroups from '../src/media-groups';
QUnit.module('MediaGroups', {
beforeEach(assert) {
this.env = useFakeEnvironment(assert);
this.clock = this.env.clock;
this.requests = this.env.requests;
},
afterEach(assert) {
this.env.restore();
}
});
QUnit.test('createMediaTypes creates skeleton object for all supported media groups',
function(assert) {
const noopToString = 'function noop() {}';
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;
const segmentLoader = {
abort: () => segmentLoaderAbortCalls++,
pause: () => segmentLoaderPauseCalls++
};
const playlistLoader = {
pause: () => playlistLoaderPauseCalls++
};
const mediaType = { activePlaylistLoader: null };
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');
mediaType.activePlaylistLoader = playlistLoader;
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');
});
QUnit.test('startLoaders starts playlist loader when appropriate',
function(assert) {
let playlistLoaderLoadCalls = 0;
let media = null;
const playlistLoader = {
load: () => playlistLoaderLoadCalls++,
media: () => media
};
const mediaType = { activePlaylistLoader: null };
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');
});
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');
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');
tracks.track3.enabled = true;
assert.strictEqual(activeTrack(), tracks.track3, 'returns active track');
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');
tracks.track1.enabled = false;
assert.strictEqual(activeTrack(), tracks.track3, 'returns active track');
tracks.track3.enabled = false;
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);
assert.equal(activeTrack(), null, 'returns null when empty track list');
tracks.track1 = { id: 'track1', mode: 'disabled' };
tracks.track2 = { id: 'track2', mode: 'hidden' };
tracks.track3 = { id: 'track3', mode: 'disabled' };
assert.equal(activeTrack(), null, 'returns null when no active tracks');
tracks.track3.mode = 'showing';
assert.strictEqual(activeTrack(), tracks.track3, 'returns active track');
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');
tracks.track1.mode = 'disabled';
assert.strictEqual(activeTrack(), tracks.track3, 'returns active track');
tracks.track3.mode = 'hidden';
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);
assert.equal(activeGroup(), null, 'returns null when no media in masterPlaylistLoader');
media = { attributes: { } };
groups.main = [{ id: 'en' }, { id: 'fr' }];
assert.strictEqual(activeGroup(), groups.main,
'defaults to main audio group when media does not specify audio group');
groups.audio = [{ id: 'en'}, { id: 'fr' }];
media.attributes.AUDIO = 'audio';
assert.strictEqual(activeGroup(), groups.audio,
'returns list of variants in active audio group');
tracks.en = { id: 'en', enabled: false };
tracks.fr = { id: 'fr', enabled: false };
assert.equal(activeGroup(activeTrack()), null,
'returns null when an active track is specified, but there is no active track');
tracks.fr.enabled = true;
assert.strictEqual(activeGroup(activeTrack()), groups.audio[1],
'returned the active group corresponding to the active track');
});
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(activeGroup(), null, 'returns null when no media in masterPlaylistLoader');
media = { attributes: { } };
// 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');
groups.subs = [{ id: 'en'}, { id: 'fr' }];
media.attributes.SUBTITLES = 'subs';
assert.strictEqual(activeGroup(), groups.subs,
'returns list of variants in active subtitle group');
tracks.en = { id: 'en', mode: 'disabled' };
tracks.fr = { id: 'fr', mode: 'disabled' };
assert.equal(activeGroup(activeTrack()), null,
'returns null when an active track is specified, but there is no active track');
tracks.fr.mode = 'showing';
assert.strictEqual(activeGroup(activeTrack()), groups.subs[1],
'returned the active group corresponding to the active track');
});
QUnit.test('onGroupChanged updates active playlist loader and resyncs segment loader',
function(assert) {
let mainSegmentLoaderResetCalls = 0;
let segmentLoaderResyncCalls = 0;
let segmentLoaderPauseCalls = 0;
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);
const onGroupChanged = MediaGroups.onGroupChanged(type, settings);
onGroupChanged();
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');
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.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');
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');
});
QUnit.test('onTrackChanged updates active playlist loader and resets segment loader',
function(assert) {
let mainSegmentLoaderResetCalls = 0;
let segmentLoaderResetCalls = 0;
let segmentLoaderPauseCalls = 0;
let segmentLoaderTrack;
const type = 'AUDIO';
const media = { attributes: { AUDIO: 'main' } };
const mainSegmentLoader = { resetEverything: () => mainSegmentLoaderResetCalls++ };
const segmentLoader = {
abort() {},
pause: () => segmentLoaderPauseCalls++,
playlist() {},
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);
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(segmentLoaderResetCalls, 0, 'no reset when no active group');
tracks.en.enabled = true;
onTrackChanged();
assert.equal(segmentLoaderPauseCalls, 2, 'loaders paused on track change');
assert.equal(mainSegmentLoaderResetCalls, 1,
'main reset changing to group with no playlist loader');
assert.equal(segmentLoaderResetCalls, 0,
'no reset 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(segmentLoaderResetCalls, 0,
'no reset when active group hasn\'t changed');
assert.strictEqual(mediaType.activePlaylistLoader, groups.main[1].playlistLoader,
'sets the correct active playlist loader');
mediaType.activePlaylistLoader = groups.main[2].playlistLoader;
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(segmentLoaderResetCalls, 1,
'reset on track change');
assert.strictEqual(mediaType.activePlaylistLoader, groups.main[1].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;
mediaType.activePlaylistLoader = groups.main[2].playlistLoader;
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(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');
});
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 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);
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('MediaGroups - initialize', {
beforeEach(assert) {
this.mediaTypes = MediaGroups.createMediaTypes();
this.master = {
mediaGroups: {
'AUDIO': {},
'SUBTITLES': {},
'CLOSED-CAPTIONS': {}
}
};
this.settings = {
mode: 'html5',
hls: {},
tech: {
addRemoteTextTrack(track) {
return { track };
}
},
segmentLoaders: {
AUDIO: { on() {} },
SUBTITLES: { on() {} }
},
requestOptions: { withCredentials: false, timeout: 10 },
master: this.master,
mediaTypes: this.mediaTypes,
blacklistCurrentPlaylist() {}
};
}
});
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');
});
QUnit.test('initialize audio correctly generates tracks and playlist loaders',
function(assert) {
const type = 'AUDIO';
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' }
};
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');
});
QUnit.test('initialize subtitles correctly generates tracks and playlist loaders',
function(assert) {
const type = 'SUBTITLES';
this.master.mediaGroups[type].sub1 = {
'en': { language: 'en', 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' }
};
MediaGroups.initialize[type](type, this.settings);
assert.deepEqual(this.mediaTypes[type].groups,
{
sub1: [
{ id: 'en', language: 'en', 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.ok(this.mediaTypes[type].tracks.fr, 'created text track');
});
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', instreamId: 'CC1' },
en708: { language: 'en', instreamId: 'SERVICE1' },
fr608: { language: 'fr', instreamId: 'CC3' },
fr708: { language: 'fr', instreamId: 'SERVICE3' }
};
MediaGroups.initialize[type](type, this.settings);
assert.deepEqual(this.mediaTypes[type].groups,
{
CCs: [
{ id: 'en608', 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');
});

146
test/videojs-contrib-hls.test.js

@ -2461,148 +2461,6 @@ QUnit.test('adds audio tracks if we have parsed some from a playlist', function(
assert.equal(vjsAudioTracks[0].enabled, false, 'main track is disabled');
});
QUnit.test('when audioinfo changes on an independent audio track in Firefox 48 & below, the enabled track is blacklisted and removed', function(assert) {
let audioTracks = this.player.audioTracks();
let oldLabel;
videojs.browser.IS_FIREFOX = true;
let origSupportsAudioInfoChange_ = videojs.Hls.supportsAudioInfoChange_;
videojs.Hls.supportsAudioInfoChange_ = () => false;
this.player.src({
src: 'manifest/multipleAudioGroups.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
let hls = this.player.tech_.hls;
let mpc = hls.masterPlaylistController_;
openMediaSource(this.player, this.clock);
// master
this.standardXHRResponse(this.requests.shift());
// media
this.standardXHRResponse(this.requests.shift());
assert.equal(audioTracks.length, 3, 'three audio track after load');
let defaultTrack = mpc.activeAudioGroup().filter((track) => {
return track.properties_.default;
})[0];
// initial audio info
hls.mediaSource.trigger({ type: 'audioinfo', info: { foo: 'bar' }});
oldLabel = audioTracks[1].label;
// simulate audio info change and mock things
audioTracks[1].enabled = true;
hls.mediaSource.trigger({ type: 'audioinfo', info: { bar: 'foo' }});
assert.equal(audioTracks.length, 2, 'two audio tracks after bad audioinfo change');
assert.notEqual(audioTracks[1].label, oldLabel, 'audio track at index 1 is not the same');
assert.equal(defaultTrack.enabled, true, 'default track is enabled again');
assert.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged');
videojs.Hls.supportsAudioInfoChange_ = origSupportsAudioInfoChange_;
});
QUnit.test('audioinfo changes with one track, blacklist playlist on Firefox 48 & below', function(assert) {
let audioTracks = this.player.audioTracks();
videojs.browser.IS_FIREFOX = true;
let origSupportsAudioInfoChange_ = videojs.Hls.supportsAudioInfoChange_;
videojs.Hls.supportsAudioInfoChange_ = () => false;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
assert.equal(audioTracks.length, 0, 'zero audio tracks at load time');
openMediaSource(this.player, this.clock);
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(audioTracks.length, 1, 'one audio track after load');
let mpc = this.player.tech_.hls.masterPlaylistController_;
let oldMedia = mpc.media();
// initial audio info
mpc.mediaSource.trigger({type: 'audioinfo', info: { foo: 'bar' }});
// simulate audio info change in main track
mpc.mediaSource.trigger({type: 'audioinfo', info: { bar: 'foo' }});
assert.equal(audioTracks.length, 1, 'still have one audio track');
assert.ok(oldMedia.excludeUntil > 0, 'blacklisted old playlist');
assert.equal(this.env.log.warn.calls, 2, 'firefox issue warning logged');
videojs.Hls.supportsAudioInfoChange_ = origSupportsAudioInfoChange_;
});
QUnit.test('changing audioinfo for muxed audio blacklists the current playlist in Firefox', function(assert) {
let audioTracks = this.player.audioTracks();
videojs.browser.IS_FIREFOX = true;
let origSupportsAudioInfoChange_ = videojs.Hls.supportsAudioInfoChange_;
videojs.Hls.supportsAudioInfoChange_ = () => false;
this.player.src({
src: 'manifest/multipleAudioGroupsCombinedMain.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.clock.tick(1);
assert.equal(audioTracks.length, 0, 'zero audio tracks at load time');
openMediaSource(this.player, this.clock);
let hls = this.player.tech_.hls;
let mpc = hls.masterPlaylistController_;
// master
this.standardXHRResponse(this.requests.shift());
// video media
this.standardXHRResponse(this.requests.shift());
// video segments
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// audio media
this.standardXHRResponse(this.requests.shift());
// ignore audio requests
this.requests.length = 0;
assert.equal(audioTracks.length, 3, 'three audio track after load');
// force audio group with combined audio to enabled
mpc.masterPlaylistLoader_.media(mpc.master().playlists[0]);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-ENDLIST\n');
let defaultTrack = mpc.activeAudioGroup().filter((track) => {
return track.properties_.default;
})[0];
let oldPlaylist = mpc.media();
// initial audio info
mpc.mediaSource.trigger({type: 'audioinfo', info: { foo: 'bar' }});
// simulate audio info change
mpc.mediaSource.trigger({type: 'audioinfo', info: { bar: 'foo' }});
audioTracks = this.player.audioTracks();
assert.equal(audioTracks.length, 3, 'three audio tracks after bad audioinfo change');
assert.equal(defaultTrack.enabled, true, 'default audio still enabled');
assert.ok(oldPlaylist.excludeUntil > 0, 'blacklisted the old playlist');
assert.equal(this.env.log.warn.calls, 2, 'firefox issue warning logged');
videojs.Hls.supportsAudioInfoChange_ = origSupportsAudioInfoChange_;
});
QUnit.test('cleans up the buffer when loading live segments', function(assert) {
let removes = [];
let seekable = videojs.createTimeRanges([[0, 70]]);
@ -2839,9 +2697,10 @@ QUnit.test('when mediaGroup changes enabled track should not change', function(a
assert.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
audioTracks = this.player.audioTracks();
let activeGroup = mpc.mediaTypes_.AUDIO.activeGroup(audioTracks[0]);
assert.equal(audioTracks.length, 3, 'three audio tracks after changing mediaGroup');
assert.ok(audioTracks[0].properties_.default, 'track one should be the default');
assert.ok(activeGroup.default, 'track one should be the default');
assert.ok(audioTracks[0].enabled, 'enabled the default track');
assert.notOk(audioTracks[1].enabled, 'disabled track two');
assert.notOk(audioTracks[2].enabled, 'disabled track three');
@ -2864,7 +2723,6 @@ QUnit.test('when mediaGroup changes enabled track should not change', function(a
assert.equal(hlsAudioChangeEvents, 1, 'an hls-audio-change event was fired');
assert.equal(audioTracks.length, 3, 'three audio tracks after reverting mediaGroup');
assert.ok(audioTracks[0].properties_.default, 'track one should be the default');
assert.notOk(audioTracks[0].enabled, 'the default track is still disabled');
assert.ok(audioTracks[1].enabled, 'track two is still enabled');
assert.notOk(audioTracks[2].enabled, 'track three is still disabled');

Loading…
Cancel
Save