Skip to content

Commit

Permalink
blacklist playlists not supported by browser media source before init…
Browse files Browse the repository at this point in the history
…ial selection (#17)
  • Loading branch information
mjneil authored and forbesjo committed Jan 24, 2018
1 parent 308efa4 commit c53225b
Show file tree
Hide file tree
Showing 8 changed files with 550 additions and 507 deletions.
2 changes: 1 addition & 1 deletion src/dash-playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
254 changes: 28 additions & 226 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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:' +
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -1254,7 +1057,6 @@ export class MasterPlaylistController extends videojs.EventTarget {
if (variantCodecs.videoCodec !== videoCodec) {
variant.excludeUntil = Infinity;
}

});
}

Expand Down
Loading

0 comments on commit c53225b

Please sign in to comment.