Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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