diff --git a/scripts/sources.json b/scripts/sources.json index e4a1a4a8a..ed05a20ba 100644 --- a/scripts/sources.json +++ b/scripts/sources.json @@ -357,5 +357,17 @@ "uri": "https://d2zihajmogu5jn.cloudfront.net/pdt-test-source/endlist.m3u8", "mimetype": "application/x-mpegurl", "features": [] + }, + { + "name": "audio only dash, two groups", + "uri": "https://d2zihajmogu5jn.cloudfront.net/audio-only-dash/dash.mpd", + "mimetype": "application/dash+xml", + "features": [] + }, + { + "name": "video only dash, two renditions", + "uri": "https://d2zihajmogu5jn.cloudfront.net/video-only-dash/dash.mpd", + "mimetype": "application/dash+xml", + "features": [] } ] diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index ca2ef5560..ddc91e73f 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -330,6 +330,53 @@ export class MasterPlaylistController extends videojs.EventTarget { this.abrTimer_ = null; } + getAudioTrackPlaylists_() { + const playlists = []; + const master = this.master(); + + if (!master && !master.mediaGroups && !master.mediaGroups.AUDIO) { + return playlists; + } + + const AUDIO = master.mediaGroups.AUDIO; + const groupKeys = Object.keys(AUDIO); + let track; + + // get the current active track + if (Object.keys(this.mediaTypes_.AUDIO.groups).length) { + track = this.mediaTypes_.AUDIO.activeTrack(); + // or get the default track from master if mediaTypes_ isn't setup yet + } else { + // default group is `main` or just the first group. + const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]]; + + for (const label in defaultGroup) { + if (defaultGroup[label].default) { + track = {label}; + break; + } + } + } + + if (!track) { + return playlists; + } + + for (const group in AUDIO) { + if (AUDIO[group][track.label]) { + const properties = AUDIO[group][track.label]; + + if (properties.playlists) { + playlists.push.apply(playlists, properties.playlists); + } else { + playlists.push(properties); + } + } + } + + return playlists; + } + /** * Register event handlers on the master playlist loader. A helper * function for construction time. diff --git a/src/media-groups.js b/src/media-groups.js index 63de67aec..2e58a23b1 100644 --- a/src/media-groups.js +++ b/src/media-groups.js @@ -2,6 +2,8 @@ import videojs from 'video.js'; import PlaylistLoader from './playlist-loader'; import DashPlaylistLoader from './dash-playlist-loader'; import noop from './util/noop'; +import {isAudioOnly} from './playlist.js'; +import logger from './util/logger'; /** * Convert the properties of an HLS track into an audioTrackKind. @@ -77,13 +79,12 @@ export const onGroupChanged = (type, settings) => () => { }, mediaTypes: { [type]: mediaType } } = settings; - const activeTrack = mediaType.activeTrack(); - const activeGroup = mediaType.activeGroup(activeTrack); + const activeGroup = mediaType.getActiveGroup(); const previousActiveLoader = mediaType.activePlaylistLoader; stopLoaders(segmentLoader, mediaType); - if (!activeGroup) { + if (!activeGroup || activeGroup.masterPlaylist) { // there is no group active return; } @@ -132,6 +133,7 @@ export const onGroupChanging = (type, settings) => () => { */ export const onTrackChanged = (type, settings) => () => { const { + masterPlaylistLoader, segmentLoaders: { [type]: segmentLoader, main: mainSegmentLoader @@ -139,8 +141,13 @@ export const onTrackChanged = (type, settings) => () => { mediaTypes: { [type]: mediaType } } = settings; const activeTrack = mediaType.activeTrack(); - const activeGroup = mediaType.activeGroup(activeTrack); + const activeGroup = mediaType.getActiveGroup(); const previousActiveLoader = mediaType.activePlaylistLoader; + const lastTrack = mediaType.lastTrack_; + + if (!lastTrack || (activeTrack && activeTrack.id !== lastTrack.id)) { + mediaType.lastTrack_ = activeTrack; + } stopLoaders(segmentLoader, mediaType); @@ -149,6 +156,18 @@ export const onTrackChanged = (type, settings) => () => { return; } + if (activeGroup.masterPlaylist) { + if (activeTrack && lastTrack && activeTrack.id !== lastTrack.id) { + const mpc = settings.vhs.masterPlaylistController_; + + mediaType.logger_(`track change. Switching master audio from ${lastTrack.id} to ${activeTrack.id}`); + masterPlaylistLoader.pause(); + mainSegmentLoader.resetEverything(); + mpc.fastQualityChange_(); + } + return; + } + if (type === 'AUDIO') { if (!activeGroup.playlistLoader) { // when switching from demuxed audio/video to muxed audio/video (noted by no @@ -375,16 +394,19 @@ export const initialize = { sourceType, segmentLoaders: { [type]: segmentLoader }, requestOptions, - master: { mediaGroups, playlists }, + master: {mediaGroups}, mediaTypes: { [type]: { groups, - tracks + tracks, + logger_ } }, masterPlaylistLoader } = settings; + const audioOnlyMaster = isAudioOnly(masterPlaylistLoader.master); + // force a default if we have none if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { @@ -395,36 +417,19 @@ export const initialize = { if (!groups[groupId]) { groups[groupId] = []; } - - // List of playlists that have an AUDIO attribute value matching the current - // group ID - const groupPlaylists = playlists.filter(playlist => { - return playlist.attributes[type] === groupId; - }); - for (const variantLabel in mediaGroups[type][groupId]) { let properties = mediaGroups[type][groupId][variantLabel]; - // List of playlists for the current group ID that do not have a matching uri - // with this alternate audio variant - const unmatchingPlaylists = groupPlaylists.filter(playlist => { - return playlist.resolvedUri !== properties.resolvedUri; - }); - - // If there are no playlists using this audio group other than ones - // that match it's uri, then the playlist is audio only. We delete the resolvedUri - // property here to prevent a playlist loader from being created so that we don't have - // both the main and audio segment loaders loading the same audio segments - // from the same playlist. - if (!unmatchingPlaylists.length && groupPlaylists.length) { - delete properties.resolvedUri; - } - let playlistLoader; - // if vhs-json was provided as the source, and the media playlist was resolved, - // use the resolved media playlist object - if (sourceType === 'vhs-json' && properties.playlists) { + if (audioOnlyMaster) { + logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a master playlist`); + properties.masterPlaylist = true; + playlistLoader = null; + + // if vhs-json was provided as the source, and the media playlist was resolved, + // use the resolved media playlist object + } else if (sourceType === 'vhs-json' && properties.playlists) { playlistLoader = new PlaylistLoader( properties.playlists[0], vhs, @@ -658,17 +663,28 @@ export const activeGroup = (type, settings) => (track) => { let variants = null; + // set to variants to main media active group if (media.attributes[type]) { variants = groups[media.attributes[type]]; } - variants = variants || groups.main; + const groupKeys = Object.keys(groups); + + if (!variants && groupKeys.length === 1) { + // use the main group if it exists + if (groups.main) { + variants = groups.main; + // only one group, use that one + } else if (groupKeys.length === 1) { + variants = groups[groupKeys[0]]; + } + } if (typeof track === 'undefined') { return variants; } - if (track === null) { + if (track === null || !variants) { // An active track was specified so a corresponding group is expected. track === null // means no track is currently active so there is no corresponding group return null; @@ -767,6 +783,15 @@ export const setupMediaGroups = (settings) => { mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); mediaTypes[type].onGroupChanging = onGroupChanging(type, settings); mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); + mediaTypes[type].getActiveGroup = () => { + const activeTrack_ = mediaTypes[type].activeTrack(); + + if (!activeTrack_) { + return null; + } + + return mediaTypes[type].activeGroup(activeTrack_); + }; }); // DO NOT enable the default subtitle or caption track. @@ -777,6 +802,7 @@ export const setupMediaGroups = (settings) => { const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id; mediaTypes.AUDIO.tracks[groupId].enabled = true; + mediaTypes.AUDIO.onGroupChanged(); mediaTypes.AUDIO.onTrackChanged(); } @@ -835,8 +861,11 @@ export const createMediaTypes = () => { activePlaylistLoader: null, activeGroup: noop, activeTrack: noop, + getActiveGroup: noop, onGroupChanged: noop, - onTrackChanged: noop + onTrackChanged: noop, + lastTrack_: null, + logger_: logger(`MediaGroups[${type}]`) }; }); diff --git a/src/playlist-selectors.js b/src/playlist-selectors.js index 70d339949..11bdba7b2 100644 --- a/src/playlist-selectors.js +++ b/src/playlist-selectors.js @@ -143,6 +143,8 @@ export const comparePlaylistResolution = function(left, right) { * Current height of the player element (should account for the device pixel ratio) * @param {boolean} limitRenditionByPlayerDimensions * True if the player width and height should be used during the selection, false otherwise + * @param {Playlist} [currentMedia] + * The currently selected playlist if it exists. * @return {Playlist} the highest bitrate playlist less than the * currently detected bandwidth, accounting for some amount of * bandwidth variance @@ -152,7 +154,8 @@ export const simpleSelector = function( playerBandwidth, playerWidth, playerHeight, - limitRenditionByPlayerDimensions + limitRenditionByPlayerDimensions, + masterPlaylistController ) { // If we end up getting called before `master` is available, exit early @@ -166,8 +169,16 @@ export const simpleSelector = function( height: playerHeight, limitRenditionByPlayerDimensions }; + + let playlists = master.playlists; + + // if playlist is audio only, select between currently active audio group playlists. + if (Playlist.isAudioOnly(master)) { + playlists = masterPlaylistController.getAudioTrackPlaylists_(); + options.audioOnly = true; + } // convert the playlists to an intermediary representation to make comparisons easier - let sortedPlaylistReps = master.playlists.map((playlist) => { + let sortedPlaylistReps = playlists.map((playlist) => { let bandwidth; const width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; const height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; @@ -320,7 +331,8 @@ export const lastBandwidthSelector = function() { this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, - this.limitRenditionByPlayerDimensions + this.limitRenditionByPlayerDimensions, + this.masterPlaylistController_ ); }; @@ -358,7 +370,8 @@ export const movingAverageBandwidthSelector = function(decay) { average, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, - this.limitRenditionByPlayerDimensions + this.limitRenditionByPlayerDimensions, + this.masterPlaylistController_ ); }; }; diff --git a/src/playlist.js b/src/playlist.js index 59a236157..8a9c8caed 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -6,6 +6,7 @@ import videojs from 'video.js'; import window from 'global/window'; import {TIME_FUDGE_FACTOR} from './ranges.js'; +import {isAudioCodec} from '@videojs/vhs-utils/es/codecs.js'; const {createTimeRange} = videojs; @@ -541,6 +542,26 @@ export const isLowestEnabledRendition = (master, media) => { }).length === 0); }; +export const isAudioOnly = (master) => { + // we are audio only if we have no main playlists but do + // have media group playlists. + if (!master.playlists.length) { + for (const groupName in master.mediaGroups.AUDIO) { + for (const label in master.mediaGroups.AUDIO[groupName]) { + const variant = master.mediaGroups.AUDIO[groupName][label]; + + if (variant.playlists && variant.playlists.length || variant.uri) { + return true; + } + } + } + } + + return master.playlists + .every((p) => p.attributes && isAudioCodec(p.attributes.CODECS)); + +}; + // exports export default { duration, @@ -555,5 +576,6 @@ export default { isAes, hasAttribute, estimateSegmentRequestTime, - isLowestEnabledRendition + isLowestEnabledRendition, + isAudioOnly }; diff --git a/src/rendition-mixin.js b/src/rendition-mixin.js index c645365bf..b48375cdc 100644 --- a/src/rendition-mixin.js +++ b/src/rendition-mixin.js @@ -1,4 +1,4 @@ -import { isIncompatible, isEnabled } from './playlist.js'; +import { isIncompatible, isEnabled, isAudioOnly } from './playlist.js'; import { codecsForPlaylist } from './util/codecs.js'; /** @@ -93,16 +93,18 @@ class Representation { * representation API into */ const renditionSelectionMixin = function(vhsHandler) { - const playlists = vhsHandler.playlists; // Add a single API-specific function to the VhsHandler instance vhsHandler.representations = () => { - if (!playlists || !playlists.master || !playlists.master.playlists) { + const master = vhsHandler.masterPlaylistController_.master(); + const playlists = isAudioOnly(master) ? + vhsHandler.masterPlaylistController_.getAudioTrackPlaylists_() : + master.playlists; + + if (!playlists) { return []; } return playlists - .master - .playlists .filter((media) => !isIncompatible(media)) .map((e, i) => new Representation(vhsHandler, e, e.id)); };