Browse Source
MediaGroups: various bug fixes and refactor (#1243)
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

committed by
GitHub

9 changed files with 1599 additions and 1048 deletions
-
597src/master-playlist-controller.js
-
751src/media-groups.js
-
28src/playlist-loader.js
-
12src/sync-controller.js
-
53src/videojs-contrib-hls.js
-
12src/vtt-segment-loader.js
-
299test/master-playlist-controller.test.js
-
749test/media-groups.test.js
-
146test/videojs-contrib-hls.test.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; |
|||
}; |
@ -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'); |
|||
}); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue