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');