diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index 14403a826..e879c274f 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -166,7 +166,7 @@ export default class DashPlaylistLoader extends EventTarget { resolveMediaGroupUris(this.master); this.trigger('loadedplaylist'); - if (!this.request) { + if (!this.media_) { // no media playlist was specifically selected so start // from the first listed one this.media(this.master.playlists[0]); diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index 18ae502a8..e6fe6197a 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -10,25 +10,20 @@ import Ranges from './ranges'; import videojs from 'video.js'; import AdCueTags from './ad-cue-tags'; import SyncController from './sync-controller'; -import { translateLegacyCodecs } from 'videojs-contrib-media-sources/es5/codec-utils'; import worker from 'webworkify'; import Decrypter from './decrypter-worker'; import Config from './config'; -import { parseCodecs } from './util/codecs.js'; +import { + parseCodecs, + mapLegacyAvcCodecs, + mimeTypesForPlaylist +} from './util/codecs.js'; import { createMediaTypes, setupMediaGroups } from './media-groups'; const ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2; let Hls; -// Default codec parameters if none were provided for video and/or audio -const defaultCodecs = { - videoCodec: 'avc1', - videoObjectTypeIndicator: '.4d400d', - // AAC-LC - audioProfile: '2' -}; - // SegmentLoader stats that need to have each loader's // values summed to calculate the final value const loaderStats = [ @@ -44,208 +39,6 @@ const sumLoaderStat = function(stat) { this.mainSegmentLoader_[stat]; }; -/** - * Replace codecs in the codec string with the old apple-style `avc1.
.
` to the - * standard `avc1.`. - * - * @param codecString {String} the codec string - * @return {String} the codec string with old apple-style codecs replaced - * - * @private - */ -export const mapLegacyAvcCodecs_ = function(codecString) { - return codecString.replace(/avc1\.(\d+)\.(\d+)/i, (match) => { - return translateLegacyCodecs([match])[0]; - }); -}; - -/** - * Build a media mime-type string from a set of parameters - * @param {String} type either 'audio' or 'video' - * @param {String} container either 'mp2t' or 'mp4' - * @param {Array} codecs an array of codec strings to add - * @return {String} a valid media mime-type - */ -const makeMimeTypeString = function(type, container, codecs) { - // The codecs array is filtered so that falsey values are - // dropped and don't cause Array#join to create spurious - // commas - return `${type}/${container}; codecs="${codecs.filter(c=>!!c).join(', ')}"`; -}; - -/** - * Returns the type container based on information in the playlist - * @param {Playlist} media the current media playlist - * @return {String} a valid media container type - */ -const getContainerType = function(media) { - // An initialization segment means the media playlist is an iframe - // playlist or is using the mp4 container. We don't currently - // support iframe playlists, so assume this is signalling mp4 - // fragments. - if (media.segments && media.segments.length && media.segments[0].map) { - return 'mp4'; - } - return 'mp2t'; -}; - -/** - * Returns a set of codec strings parsed from the playlist or the default - * codec strings if no codecs were specified in the playlist - * @param {Playlist} media the current media playlist - * @return {Object} an object with the video and audio codecs - */ -const getCodecs = function(media) { - // if the codecs were explicitly specified, use them instead of the - // defaults - let mediaAttributes = media.attributes || {}; - - if (mediaAttributes.CODECS) { - return parseCodecs(mediaAttributes.CODECS); - } - return defaultCodecs; -}; - -const audioProfileFromDefault = (master, audioGroupId) => { - if (!master.mediaGroups.AUDIO || !audioGroupId) { - return null; - } - - const audioGroup = master.mediaGroups.AUDIO[audioGroupId]; - - if (!audioGroup) { - return null; - } - - for (let name in audioGroup) { - const audioType = audioGroup[name]; - - if (audioType.default && audioType.playlists) { - // codec should be the same for all playlists within the audio type - return parseCodecs(audioType.playlists[0].attributes.CODECS).audioProfile; - } - } - - return null; -}; - -/** - * Calculates the MIME type strings for a working configuration of - * SourceBuffers to play variant streams in a master playlist. If - * there is no possible working configuration, an empty array will be - * returned. - * - * @param master {Object} the m3u8 object for the master playlist - * @param media {Object} the m3u8 object for the variant playlist - * @return {Array} the MIME type strings. If the array has more than - * one entry, the first element should be applied to the video - * SourceBuffer and the second to the audio SourceBuffer. - * - * @private - */ -export const mimeTypesForPlaylist_ = function(master, media) { - let containerType = getContainerType(media); - let codecInfo = getCodecs(media); - let mediaAttributes = media.attributes || {}; - // Default condition for a traditional HLS (no demuxed audio/video) - let isMuxed = true; - let isMaat = false; - - if (!media) { - // Not enough information - return []; - } - - if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) { - let audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO]; - - // Handle the case where we are in a multiple-audio track scenario - if (audioGroup) { - isMaat = true; - // Start with the everything demuxed then... - isMuxed = false; - // ...check to see if any audio group tracks are muxed (ie. lacking a uri) - for (let groupId in audioGroup) { - // either a uri is present (if the case of HLS and an external playlist), or - // playlists is present (in the case of DASH where we don't have external audio - // playlists) - if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { - isMuxed = true; - break; - } - } - } - } - - // HLS with multiple-audio tracks must always get an audio codec. - // Put another way, there is no way to have a video-only multiple-audio HLS! - if (isMaat && !codecInfo.audioProfile) { - if (!isMuxed) { - // It is possible for codecs to be specified on the audio media group playlist but - // not on the rendition playlist. This is mostly the case for DASH, where audio and - // video are always separate (and separately specified). - codecInfo.audioProfile = audioProfileFromDefault(master, mediaAttributes.AUDIO); - } - - if (!codecInfo.audioProfile) { - videojs.log.warn( - 'Multiple audio tracks present but no audio codec string is specified. ' + - 'Attempting to use the default audio codec (mp4a.40.2)'); - codecInfo.audioProfile = defaultCodecs.audioProfile; - } - } - - // Generate the final codec strings from the codec object generated above - let codecStrings = {}; - - if (codecInfo.videoCodec) { - codecStrings.video = `${codecInfo.videoCodec}${codecInfo.videoObjectTypeIndicator}`; - } - - if (codecInfo.audioProfile) { - codecStrings.audio = `mp4a.40.${codecInfo.audioProfile}`; - } - - // Finally, make and return an array with proper mime-types depending on - // the configuration - let justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]); - let justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]); - let bothVideoAudio = makeMimeTypeString('video', containerType, [ - codecStrings.video, - codecStrings.audio - ]); - - if (isMaat) { - if (!isMuxed && codecStrings.video) { - return [ - justVideo, - justAudio - ]; - } - // There exists the possiblity that this will return a `video/container` - // mime-type for the first entry in the array even when there is only audio. - // This doesn't appear to be a problem and simplifies the code. - return [ - bothVideoAudio, - justAudio - ]; - } - - // If there is ano video codec at all, always just return a single - // audio/ mime-type - if (!codecStrings.video) { - return [ - justAudio - ]; - } - - // When not using separate audio media groups, audio and video is - // *always* muxed - return [ - bothVideoAudio - ]; -}; - /** * the master playlist controller controller all interactons * between playlists and segmentloaders. At this time this mainly @@ -425,6 +218,10 @@ export class MasterPlaylistController extends videojs.EventTarget { let updatedPlaylist = this.masterPlaylistLoader_.media(); if (!updatedPlaylist) { + // blacklist any variants that are not supported by the browser before selecting + // an initial media as the playlist selectors do not consider browser support + this.excludeUnsupportedVariants_(); + let selectedMedia; if (this.enableLowInitialPlaylist) { @@ -1168,7 +965,7 @@ export class MasterPlaylistController extends videojs.EventTarget { return; } - mimeTypes = mimeTypesForPlaylist_(this.masterPlaylistLoader_.master, media); + mimeTypes = mimeTypesForPlaylist(this.masterPlaylistLoader_.master, media); if (mimeTypes.length < 1) { this.error = 'No compatible SourceBuffer configuration for the variant stream:' + @@ -1199,6 +996,21 @@ export class MasterPlaylistController extends videojs.EventTarget { } } + /** + * Blacklists playlists with codecs that are unsupported by the browser. + */ + excludeUnsupportedVariants_() { + this.master().playlists.forEach(variant => { + if (variant.attributes.CODECS && + window.MediaSource && + window.MediaSource.isTypeSupported && + !window.MediaSource.isTypeSupported( + `video/mp4; codecs="${mapLegacyAvcCodecs(variant.attributes.CODECS)}"`)) { + variant.excludeUntil = Infinity; + } + }); + } + /** * Blacklist playlists that are known to be codec or * stream-incompatible with the SourceBuffer configuration. For @@ -1214,7 +1026,6 @@ export class MasterPlaylistController extends videojs.EventTarget { * @private */ excludeIncompatibleVariants_(media) { - let master = this.masterPlaylistLoader_.master; let codecCount = 2; let videoCodec = null; let codecs; @@ -1224,23 +1035,15 @@ export class MasterPlaylistController extends videojs.EventTarget { videoCodec = codecs.videoCodec; codecCount = codecs.codecCount; } - master.playlists.forEach(function(variant) { + + this.master().playlists.forEach(function(variant) { let variantCodecs = { codecCount: 2, videoCodec: null }; if (variant.attributes.CODECS) { - let codecString = variant.attributes.CODECS; - - variantCodecs = parseCodecs(codecString); - - if (window.MediaSource && - window.MediaSource.isTypeSupported && - !window.MediaSource.isTypeSupported( - 'video/mp4; codecs="' + mapLegacyAvcCodecs_(codecString) + '"')) { - variant.excludeUntil = Infinity; - } + variantCodecs = parseCodecs(variant.attributes.CODECS); } // if the streams differ in the presence or absence of audio or @@ -1254,7 +1057,6 @@ export class MasterPlaylistController extends videojs.EventTarget { if (variantCodecs.videoCodec !== videoCodec) { variant.excludeUntil = Infinity; } - }); } diff --git a/src/util/codecs.js b/src/util/codecs.js index ba4f48642..f9791b214 100644 --- a/src/util/codecs.js +++ b/src/util/codecs.js @@ -1,9 +1,19 @@ - /** * @file - codecs.js - Handles tasks regarding codec strings such as translating them to * codec strings, or translating codec strings into objects that can be examined. */ +import videojs from 'video.js'; +import { translateLegacyCodecs } from 'videojs-contrib-media-sources/es5/codec-utils'; + +// Default codec parameters if none were provided for video and/or audio +const defaultCodecs = { + videoCodec: 'avc1', + videoObjectTypeIndicator: '.4d400d', + // AAC-LC + audioProfile: '2' +}; + /** * Parses a codec string to retrieve the number of codecs specified, * the video codec and object type indicator, and the audio profile. @@ -19,7 +29,7 @@ export const parseCodecs = function(codecs = '') { result.codecCount = result.codecCount || 2; // parse the video codec - parsed = (/(^|\s|,)+(avc1)([^ ,]*)/i).exec(codecs); + parsed = (/(^|\s|,)+(avc[13])([^ ,]*)/i).exec(codecs); if (parsed) { result.videoCodec = parsed[2]; result.videoObjectTypeIndicator = parsed[3]; @@ -32,3 +42,205 @@ export const parseCodecs = function(codecs = '') { return result; }; + +/** + * Replace codecs in the codec string with the old apple-style `avc1.
.
` to the + * standard `avc1.`. + * + * @param codecString {String} the codec string + * @return {String} the codec string with old apple-style codecs replaced + * + * @private + */ +export const mapLegacyAvcCodecs = function(codecString) { + return codecString.replace(/avc1\.(\d+)\.(\d+)/i, (match) => { + return translateLegacyCodecs([match])[0]; + }); +}; + +/** + * Build a media mime-type string from a set of parameters + * @param {String} type either 'audio' or 'video' + * @param {String} container either 'mp2t' or 'mp4' + * @param {Array} codecs an array of codec strings to add + * @return {String} a valid media mime-type + */ +export const makeMimeTypeString = function(type, container, codecs) { + // The codecs array is filtered so that falsey values are + // dropped and don't cause Array#join to create spurious + // commas + return `${type}/${container}; codecs="${codecs.filter(c=>!!c).join(', ')}"`; +}; + +/** + * Returns the type container based on information in the playlist + * @param {Playlist} media the current media playlist + * @return {String} a valid media container type + */ +export const getContainerType = function(media) { + // An initialization segment means the media playlist is an iframe + // playlist or is using the mp4 container. We don't currently + // support iframe playlists, so assume this is signalling mp4 + // fragments. + if (media.segments && media.segments.length && media.segments[0].map) { + return 'mp4'; + } + return 'mp2t'; +}; + +/** + * Returns a set of codec strings parsed from the playlist or the default + * codec strings if no codecs were specified in the playlist + * @param {Playlist} media the current media playlist + * @return {Object} an object with the video and audio codecs + */ +const getCodecs = function(media) { + // if the codecs were explicitly specified, use them instead of the + // defaults + let mediaAttributes = media.attributes || {}; + + if (mediaAttributes.CODECS) { + return parseCodecs(mediaAttributes.CODECS); + } + return defaultCodecs; +}; + +const audioProfileFromDefault = (master, audioGroupId) => { + if (!master.mediaGroups.AUDIO || !audioGroupId) { + return null; + } + + const audioGroup = master.mediaGroups.AUDIO[audioGroupId]; + + if (!audioGroup) { + return null; + } + + for (let name in audioGroup) { + const audioType = audioGroup[name]; + + if (audioType.default && audioType.playlists) { + // codec should be the same for all playlists within the audio type + return parseCodecs(audioType.playlists[0].attributes.CODECS).audioProfile; + } + } + + return null; +}; + +/** + * Calculates the MIME type strings for a working configuration of + * SourceBuffers to play variant streams in a master playlist. If + * there is no possible working configuration, an empty array will be + * returned. + * + * @param master {Object} the m3u8 object for the master playlist + * @param media {Object} the m3u8 object for the variant playlist + * @return {Array} the MIME type strings. If the array has more than + * one entry, the first element should be applied to the video + * SourceBuffer and the second to the audio SourceBuffer. + * + * @private + */ +export const mimeTypesForPlaylist = function(master, media) { + let containerType = getContainerType(media); + let codecInfo = getCodecs(media); + let mediaAttributes = media.attributes || {}; + // Default condition for a traditional HLS (no demuxed audio/video) + let isMuxed = true; + let isMaat = false; + + if (!media) { + // Not enough information + return []; + } + + if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) { + let audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO]; + + // Handle the case where we are in a multiple-audio track scenario + if (audioGroup) { + isMaat = true; + // Start with the everything demuxed then... + isMuxed = false; + // ...check to see if any audio group tracks are muxed (ie. lacking a uri) + for (let groupId in audioGroup) { + // either a uri is present (if the case of HLS and an external playlist), or + // playlists is present (in the case of DASH where we don't have external audio + // playlists) + if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { + isMuxed = true; + break; + } + } + } + } + + // HLS with multiple-audio tracks must always get an audio codec. + // Put another way, there is no way to have a video-only multiple-audio HLS! + if (isMaat && !codecInfo.audioProfile) { + if (!isMuxed) { + // It is possible for codecs to be specified on the audio media group playlist but + // not on the rendition playlist. This is mostly the case for DASH, where audio and + // video are always separate (and separately specified). + codecInfo.audioProfile = audioProfileFromDefault(master, mediaAttributes.AUDIO); + } + + if (!codecInfo.audioProfile) { + videojs.log.warn( + 'Multiple audio tracks present but no audio codec string is specified. ' + + 'Attempting to use the default audio codec (mp4a.40.2)'); + codecInfo.audioProfile = defaultCodecs.audioProfile; + } + } + + // Generate the final codec strings from the codec object generated above + let codecStrings = {}; + + if (codecInfo.videoCodec) { + codecStrings.video = `${codecInfo.videoCodec}${codecInfo.videoObjectTypeIndicator}`; + } + + if (codecInfo.audioProfile) { + codecStrings.audio = `mp4a.40.${codecInfo.audioProfile}`; + } + + // Finally, make and return an array with proper mime-types depending on + // the configuration + let justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]); + let justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]); + let bothVideoAudio = makeMimeTypeString('video', containerType, [ + codecStrings.video, + codecStrings.audio + ]); + + if (isMaat) { + if (!isMuxed && codecStrings.video) { + return [ + justVideo, + justAudio + ]; + } + // There exists the possiblity that this will return a `video/container` + // mime-type for the first entry in the array even when there is only audio. + // This doesn't appear to be a problem and simplifies the code. + return [ + bothVideoAudio, + justAudio + ]; + } + + // If there is ano video codec at all, always just return a single + // audio/ mime-type + if (!codecStrings.video) { + return [ + justAudio + ]; + } + + // When not using separate audio media groups, audio and video is + // *always* muxed + return [ + bothVideoAudio + ]; +}; diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index 4e3832a4e..29697acd5 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -163,16 +163,17 @@ const emeOptions = (keySystemOptions, videoPlaylist, audioPlaylist) => { }; const setupEmeOptions = (hlsHandler) => { - if (hlsHandler.options_.sourceType === 'dash') { - const player = videojs.players[hlsHandler.tech_.options_.playerId]; - - if (player.eme) { - player.eme.options = emeOptions( - hlsHandler.source_.keySystems, - hlsHandler.playlists.media(), - hlsHandler.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader.media() - ); - } + if (hlsHandler.options_.sourceType !== 'dash') { + return; + } + const player = videojs.players[hlsHandler.tech_.options_.playerId]; + + if (player.eme) { + player.eme.options = emeOptions( + hlsHandler.source_.keySystems, + hlsHandler.playlists.media(), + hlsHandler.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader.media() + ); } }; @@ -649,10 +650,13 @@ const HlsSourceHandler = function(mode) { localOptions.hls.mode !== mode) { return false; } - return HlsSourceHandler.canPlayType(srcObj.type, videojs.mergeOptions(localOptions, sourceHandlerOptions)); + return HlsSourceHandler.canPlayType(srcObj.type, + videojs.mergeOptions(localOptions, sourceHandlerOptions)); }, handleSource(source, tech, options = {}) { - let localOptions = videojs.mergeOptions(videojs.options, options, sourceHandlerOptions); + let localOptions = videojs.mergeOptions(videojs.options, + options, + sourceHandlerOptions); if (mode === 'flash') { // We need to trigger this asynchronously to give others the chance @@ -669,7 +673,9 @@ const HlsSourceHandler = function(mode) { return tech.hls; }, canPlayType(type, options = {}) { - let localOptions = videojs.mergeOptions(videojs.options, sourceHandlerOptions, options); + let localOptions = videojs.mergeOptions(videojs.options, + sourceHandlerOptions, + options); if (HlsSourceHandler.canPlayType(type, localOptions)) { return 'maybe'; diff --git a/test/codecs.test.js b/test/codecs.test.js new file mode 100644 index 000000000..8e86d1279 --- /dev/null +++ b/test/codecs.test.js @@ -0,0 +1,258 @@ +import QUnit from 'qunit'; +import { mimeTypesForPlaylist, mapLegacyAvcCodecs } from '../src/util/codecs'; + +const generateMedia = function(isMaat, isMuxed, hasVideoCodec, hasAudioCodec, isFMP4) { + const codec = (hasVideoCodec ? 'avc1.deadbeef' : '') + + (hasVideoCodec && hasAudioCodec ? ',' : '') + + (hasAudioCodec ? 'mp4a.40.E' : ''); + const master = { + mediaGroups: {}, + playlists: [] + }; + const media = { + attributes: {} + }; + + if (isMaat) { + master.mediaGroups.AUDIO = { + test: { + demuxed: { + uri: 'foo.bar' + } + } + }; + + if (isMuxed) { + master.mediaGroups.AUDIO.test.muxed = {}; + } + media.attributes.AUDIO = 'test'; + } + + if (isFMP4) { + // This is not a great way to signal that the playlist is fmp4 but + // this is how we currently detect it in HLS so let's emulate it here + media.segments = [ + { + map: 'test' + } + ]; + } + + if (hasVideoCodec || hasAudioCodec) { + media.attributes.CODECS = codec; + } + + return [master, media]; +}; + +QUnit.module('Codec to MIME Type Conversion'); + +const testMimeTypes = function(assert, isFMP4) { + let container = isFMP4 ? 'mp4' : 'mp2t'; + + let videoMime = `video/${container}`; + let audioMime = `audio/${container}`; + + // no MAAT + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(false, true, false, false, isFMP4)), + [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`], + `no MAAT, container: ${container}, codecs: none`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(false, true, true, false, isFMP4)), + [`${videoMime}; codecs="avc1.deadbeef"`], + `no MAAT, container: ${container}, codecs: video`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(false, true, false, true, isFMP4)), + [`${audioMime}; codecs="mp4a.40.E"`], + `no MAAT, container: ${container}, codecs: audio`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(false, true, true, true, isFMP4)), + [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`], + `no MAAT, container: ${container}, codecs: video, audio`); + + // MAAT, not muxed + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, false, false, false, isFMP4)), + [`${videoMime}; codecs="avc1.4d400d"`, + `${audioMime}; codecs="mp4a.40.2"`], + `MAAT, demuxed, container: ${container}, codecs: none`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, false, true, false, isFMP4)), + [`${videoMime}; codecs="avc1.deadbeef"`, + `${audioMime}; codecs="mp4a.40.2"`], + `MAAT, demuxed, container: ${container}, codecs: video`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, false, false, true, isFMP4)), + [`${videoMime}; codecs="mp4a.40.E"`, + `${audioMime}; codecs="mp4a.40.E"`], + `MAAT, demuxed, container: ${container}, codecs: audio`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, false, true, true, isFMP4)), + [`${videoMime}; codecs="avc1.deadbeef"`, + `${audioMime}; codecs="mp4a.40.E"`], + `MAAT, demuxed, container: ${container}, codecs: video, audio`); + + // MAAT, muxed + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, true, false, false, isFMP4)), + [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`, + `${audioMime}; codecs="mp4a.40.2"`], + `MAAT, muxed, container: ${container}, codecs: none`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, true, true, false, isFMP4)), + [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.2"`, + `${audioMime}; codecs="mp4a.40.2"`], + `MAAT, muxed, container: ${container}, codecs: video`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, true, false, true, isFMP4)), + [`${videoMime}; codecs="mp4a.40.E"`, + `${audioMime}; codecs="mp4a.40.E"`], + `MAAT, muxed, container: ${container}, codecs: audio`); + + assert.deepEqual(mimeTypesForPlaylist.apply(null, + generateMedia(true, true, true, true, isFMP4)), + [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`, + `${audioMime}; codecs="mp4a.40.E"`], + `MAAT, muxed, container: ${container}, codecs: video, audio`); +}; + +QUnit.test('recognizes muxed codec configurations', function(assert) { + testMimeTypes(assert, false); + testMimeTypes(assert, true); +}); + +// dash audio playlist won't have a URI but will have resolved playlists +QUnit.test('content demuxed if alt audio URI not present but playlists present', +function(assert) { + const media = { + attributes: { + AUDIO: 'test', + CODECS: 'avc1.deadbeef, mp4a.40.E' + }, + segments: [ + // signal fmp4 + { map: 'test' } + ] + }; + const master = { + mediaGroups: { + AUDIO: { + test: { + demuxed: { + uri: 'foo.bar' + } + } + } + }, + playlists: [media] + }; + + assert.deepEqual(mimeTypesForPlaylist(master, media), + ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.E"'], + 'demuxed if URI'); + + delete master.mediaGroups.AUDIO.test.demuxed.uri; + assert.deepEqual( + mimeTypesForPlaylist(master, media), + ['video/mp4; codecs="avc1.deadbeef, mp4a.40.E"', 'audio/mp4; codecs="mp4a.40.E"'], + 'muxed if no URI and no playlists'); + + master.mediaGroups.AUDIO.test.demuxed.playlists = [{}]; + assert.deepEqual(mimeTypesForPlaylist(master, media), + ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.E"'], + 'demuxed if no URI but playlists'); +}); + +QUnit.test('uses audio codec from default group if not specified in media attributes', +function(assert) { + const media = { + attributes: { + AUDIO: 'test', + CODECS: 'avc1.deadbeef' + }, + segments: [ + // signal fmp4 + { map: 'test' } + ] + }; + // dash audio playlist won't have a URI but will have resolved playlists + const master = { + mediaGroups: { + AUDIO: { + test: { + demuxed: { + default: true, + playlists: [{ + attributes: { + CODECS: 'mp4a.40.E' + } + }] + } + } + } + }, + playlists: [media] + }; + + assert.deepEqual( + mimeTypesForPlaylist(master, media), + ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.E"'], + 'uses audio codec from media group'); + + delete master.mediaGroups.AUDIO.test.demuxed.default; + assert.deepEqual( + mimeTypesForPlaylist(master, media), + ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.2"'], + 'uses default audio codec'); +}); + +QUnit.module('Map Legacy AVC Codec'); + +QUnit.test('maps legacy AVC codecs', function(assert) { + assert.equal(mapLegacyAvcCodecs('avc1.deadbeef'), + 'avc1.deadbeef', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('avc1.dead.beef, mp4a.something'), + 'avc1.dead.beef, mp4a.something', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('avc1.dead.beef,mp4a.something'), + 'avc1.dead.beef,mp4a.something', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('mp4a.something,avc1.dead.beef'), + 'mp4a.something,avc1.dead.beef', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('mp4a.something, avc1.dead.beef'), + 'mp4a.something, avc1.dead.beef', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('avc1.42001e'), + 'avc1.42001e', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('avc1.4d0020,mp4a.40.2'), + 'avc1.4d0020,mp4a.40.2', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('mp4a.40.2,avc1.4d0020'), + 'mp4a.40.2,avc1.4d0020', + 'does nothing for non legacy pattern'); + assert.equal(mapLegacyAvcCodecs('mp4a.40.40'), + 'mp4a.40.40', + 'does nothing for non video codecs'); + + assert.equal(mapLegacyAvcCodecs('avc1.66.30'), + 'avc1.42001e', + 'translates legacy video codec alone'); + assert.equal(mapLegacyAvcCodecs('avc1.66.30, mp4a.40.2'), + 'avc1.42001e, mp4a.40.2', + 'translates legacy video codec when paired with audio'); + assert.equal(mapLegacyAvcCodecs('mp4a.40.2, avc1.66.30'), + 'mp4a.40.2, avc1.42001e', + 'translates video codec when specified second'); +}); diff --git a/test/configuration.test.js b/test/configuration.test.js index 41ae0c07d..1b8124d05 100644 --- a/test/configuration.test.js +++ b/test/configuration.test.js @@ -377,7 +377,8 @@ QUnit.test('DASH can be handled', function(assert) { let flashCanHandleSource = new HlsSourceHandler('flash').canHandleSource; assert.ok(htmlCanHandleSource({type: 'application/dash+xml'}), 'supported with MSE'); - assert.notOk(flashCanHandleSource({type: 'application/dash+xml'}), 'not supported in Flash'); + assert.notOk(flashCanHandleSource({type: 'application/dash+xml'}), + 'not supported in Flash'); }); QUnit.test('global mode override - flash', function(assert) { diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index 599082815..2d7d49f11 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -8,11 +8,7 @@ import { openMediaSource } from './test-helpers.js'; import manifests from './test-manifests.js'; -import { - MasterPlaylistController, - mimeTypesForPlaylist_, - mapLegacyAvcCodecs_ -} from '../src/master-playlist-controller'; +import { MasterPlaylistController } from '../src/master-playlist-controller'; /* eslint-disable no-unused-vars */ // we need this so that it can register hls with videojs import { Hls } from '../src/videojs-http-streaming'; @@ -22,50 +18,6 @@ import Config from '../src/config'; import PlaylistLoader from '../src/playlist-loader'; import DashPlaylistLoader from '../src/dash-playlist-loader'; -const generateMedia = function(isMaat, isMuxed, hasVideoCodec, hasAudioCodec, isFMP4) { - const codec = (hasVideoCodec ? 'avc1.deadbeef' : '') + - (hasVideoCodec && hasAudioCodec ? ',' : '') + - (hasAudioCodec ? 'mp4a.40.E' : ''); - const master = { - mediaGroups: {}, - playlists: [] - }; - const media = { - attributes: {} - }; - - if (isMaat) { - master.mediaGroups.AUDIO = { - test: { - demuxed: { - uri: 'foo.bar' - } - } - }; - - if (isMuxed) { - master.mediaGroups.AUDIO.test.muxed = {}; - } - media.attributes.AUDIO = 'test'; - } - - if (isFMP4) { - // This is not a great way to signal that the playlist is fmp4 but - // this is how we currently detect it in HLS so let's emulate it here - media.segments = [ - { - map: 'test' - } - ]; - } - - if (hasVideoCodec || hasAudioCodec) { - media.attributes.CODECS = codec; - } - - return [master, media]; -}; - QUnit.module('MasterPlaylistController', { beforeEach(assert) { this.env = useFakeEnvironment(assert); @@ -858,7 +810,7 @@ function(assert) { assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above'); }); -QUnit.test('blacklists switching between playlists with incompatible audio codecs', +QUnit.test('does not blacklist switching between playlists with different audio profiles', function(assert) { let alternatePlaylist; @@ -883,11 +835,34 @@ function(assert) { this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1]; assert.equal(alternatePlaylist.excludeUntil, undefined, - 'not excluded incompatible playlist'); + 'not excluded playlist'); // verify stats assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above'); }); +QUnit.test('blacklists playlists with unsupported codecs before initial selection', +function(assert) { + this.masterPlaylistController.selectPlaylist = () => { + assert.equal( + this.masterPlaylistController.master().playlists[0].excludeUntil, + Infinity, + 'Blacklists unsupported playlist before initial selection'); + }; + + openMediaSource(this.player, this.clock); + + // master + this.requests.shift().respond(200, null, + '#EXTM3U\n' + + '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="unsupporte.dc0dec,mp4a.40.5"\n' + + 'media.m3u8\n' + + '#EXT-X-STREAM-INF:BANDWIDTH=10000,CODECS="avc1.4d400d,mp4a.40.2"\n' + + 'media1.m3u8\n'); + + // media + this.standardXHRResponse(this.requests.shift()); +}); + QUnit.test('updates the combined segment loader on media changes', function(assert) { let updates = []; @@ -2396,215 +2371,3 @@ QUnit.test('properly configures loader mime types', function(assert) { assert.ok(audioMimeTypeCalls[0][1] instanceof videojs.EventTarget, 'passed a source buffer emitter to audio segment loader'); }); - -QUnit.module('Codec to MIME Type Conversion'); - -const testMimeTypes = function(assert, isFMP4) { - let container = isFMP4 ? 'mp4' : 'mp2t'; - - let videoMime = `video/${container}`; - let audioMime = `audio/${container}`; - - // no MAAT - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(false, true, false, false, isFMP4)), - [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`], - `no MAAT, container: ${container}, codecs: none`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(false, true, true, false, isFMP4)), - [`${videoMime}; codecs="avc1.deadbeef"`], - `no MAAT, container: ${container}, codecs: video`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(false, true, false, true, isFMP4)), - [`${audioMime}; codecs="mp4a.40.E"`], - `no MAAT, container: ${container}, codecs: audio`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(false, true, true, true, isFMP4)), - [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`], - `no MAAT, container: ${container}, codecs: video, audio`); - - // MAAT, not muxed - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, false, false, false, isFMP4)), - [`${videoMime}; codecs="avc1.4d400d"`, - `${audioMime}; codecs="mp4a.40.2"`], - `MAAT, demuxed, container: ${container}, codecs: none`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, false, true, false, isFMP4)), - [`${videoMime}; codecs="avc1.deadbeef"`, - `${audioMime}; codecs="mp4a.40.2"`], - `MAAT, demuxed, container: ${container}, codecs: video`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, false, false, true, isFMP4)), - [`${videoMime}; codecs="mp4a.40.E"`, - `${audioMime}; codecs="mp4a.40.E"`], - `MAAT, demuxed, container: ${container}, codecs: audio`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, false, true, true, isFMP4)), - [`${videoMime}; codecs="avc1.deadbeef"`, - `${audioMime}; codecs="mp4a.40.E"`], - `MAAT, demuxed, container: ${container}, codecs: video, audio`); - - // MAAT, muxed - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, true, false, false, isFMP4)), - [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`, - `${audioMime}; codecs="mp4a.40.2"`], - `MAAT, muxed, container: ${container}, codecs: none`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, true, true, false, isFMP4)), - [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.2"`, - `${audioMime}; codecs="mp4a.40.2"`], - `MAAT, muxed, container: ${container}, codecs: video`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, true, false, true, isFMP4)), - [`${videoMime}; codecs="mp4a.40.E"`, - `${audioMime}; codecs="mp4a.40.E"`], - `MAAT, muxed, container: ${container}, codecs: audio`); - - assert.deepEqual(mimeTypesForPlaylist_.apply(null, - generateMedia(true, true, true, true, isFMP4)), - [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`, - `${audioMime}; codecs="mp4a.40.E"`], - `MAAT, muxed, container: ${container}, codecs: video, audio`); -}; - -QUnit.test('recognizes muxed codec configurations', function(assert) { - testMimeTypes(assert, false); - testMimeTypes(assert, true); -}); - -// dash audio playlist won't have a URI but will have resolved playlists -QUnit.test('content demuxed if alt audio URI not present but playlists present', -function(assert) { - const media = { - attributes: { - AUDIO: 'test', - CODECS: 'avc1.deadbeef, mp4a.40.E' - }, - segments: [ - // signal fmp4 - { map: 'test' } - ] - }; - const master = { - mediaGroups: { - AUDIO: { - test: { - demuxed: { - uri: 'foo.bar' - } - } - } - }, - playlists: [media] - }; - - assert.deepEqual(mimeTypesForPlaylist_(master, media), - ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.E"'], - 'demuxed if URI'); - - delete master.mediaGroups.AUDIO.test.demuxed.uri; - assert.deepEqual( - mimeTypesForPlaylist_(master, media), - ['video/mp4; codecs="avc1.deadbeef, mp4a.40.E"', 'audio/mp4; codecs="mp4a.40.E"'], - 'muxed if no URI and no playlists'); - - master.mediaGroups.AUDIO.test.demuxed.playlists = [{}]; - assert.deepEqual(mimeTypesForPlaylist_(master, media), - ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.E"'], - 'demuxed if no URI but playlists'); -}); - -QUnit.test('uses audio codec from default group if not specified in media attributes', -function(assert) { - const media = { - attributes: { - AUDIO: 'test', - CODECS: 'avc1.deadbeef' - }, - segments: [ - // signal fmp4 - { map: 'test' } - ] - }; - // dash audio playlist won't have a URI but will have resolved playlists - const master = { - mediaGroups: { - AUDIO: { - test: { - demuxed: { - default: true, - playlists: [{ - attributes: { - CODECS: 'mp4a.40.E' - } - }] - } - } - } - }, - playlists: [media] - }; - - assert.deepEqual( - mimeTypesForPlaylist_(master, media), - ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.E"'], - 'uses audio codec from media group'); - - delete master.mediaGroups.AUDIO.test.demuxed.default; - assert.deepEqual( - mimeTypesForPlaylist_(master, media), - ['video/mp4; codecs="avc1.deadbeef"', 'audio/mp4; codecs="mp4a.40.2"'], - 'uses default audio codec'); -}); - -QUnit.module('Map Legacy AVC Codec'); - -QUnit.test('maps legacy AVC codecs', function(assert) { - assert.equal(mapLegacyAvcCodecs_('avc1.deadbeef'), - 'avc1.deadbeef', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('avc1.dead.beef, mp4a.something'), - 'avc1.dead.beef, mp4a.something', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('avc1.dead.beef,mp4a.something'), - 'avc1.dead.beef,mp4a.something', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('mp4a.something,avc1.dead.beef'), - 'mp4a.something,avc1.dead.beef', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('mp4a.something, avc1.dead.beef'), - 'mp4a.something, avc1.dead.beef', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('avc1.42001e'), - 'avc1.42001e', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('avc1.4d0020,mp4a.40.2'), - 'avc1.4d0020,mp4a.40.2', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('mp4a.40.2,avc1.4d0020'), - 'mp4a.40.2,avc1.4d0020', - 'does nothing for non legacy pattern'); - assert.equal(mapLegacyAvcCodecs_('mp4a.40.40'), - 'mp4a.40.40', - 'does nothing for non video codecs'); - - assert.equal(mapLegacyAvcCodecs_('avc1.66.30'), - 'avc1.42001e', - 'translates legacy video codec alone'); - assert.equal(mapLegacyAvcCodecs_('avc1.66.30, mp4a.40.2'), - 'avc1.42001e, mp4a.40.2', - 'translates legacy video codec when paired with audio'); - assert.equal(mapLegacyAvcCodecs_('mp4a.40.2, avc1.66.30'), - 'mp4a.40.2, avc1.42001e', - 'translates video codec when specified second'); -}); diff --git a/test/ranges.test.js b/test/ranges.test.js index 8432c9cd5..3c2e888f3 100644 --- a/test/ranges.test.js +++ b/test/ranges.test.js @@ -333,7 +333,8 @@ QUnit.test('creates printable ranges', function(assert) { }); QUnit.test('converts time ranges to an array', function(assert) { - assert.deepEqual(Ranges.timeRangesToArray(createTimeRanges()), [], 'empty range empty array'); + assert.deepEqual(Ranges.timeRangesToArray(createTimeRanges()), [], + 'empty range empty array'); assert.deepEqual(Ranges.timeRangesToArray(createTimeRanges([[0, 1]])), [{start: 0, end: 1}], 'formats range correctly');