Browse Source

blacklist playlists not supported by browser media source before initial selection (#17)

pull/22/head
Matthew Neil 8 years ago
committed by Joe Forbes
parent
commit
c53225b2f8
  1. 2
      src/dash-playlist-loader.js
  2. 254
      src/master-playlist-controller.js
  3. 216
      src/util/codecs.js
  4. 32
      src/videojs-http-streaming.js
  5. 258
      test/codecs.test.js
  6. 3
      test/configuration.test.js
  7. 289
      test/master-playlist-controller.test.js
  8. 3
      test/ranges.test.js

2
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]);

254
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.<dd>.<dd>` to the
* standard `avc1.<hhhhhh>`.
*
* @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/<container> 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;
}
});
}

216
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.<dd>.<dd>` to the
* standard `avc1.<hhhhhh>`.
*
* @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/<container> mime-type
if (!codecStrings.video) {
return [
justAudio
];
}
// When not using separate audio media groups, audio and video is
// *always* muxed
return [
bothVideoAudio
];
};

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

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

3
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) {

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

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

Loading…
Cancel
Save