Skip to content
This repository has been archived by the owner on Jan 12, 2019. It is now read-only.

Commit

Permalink
fix: CODEC to mime-type conversion now takes into account all possibl…
Browse files Browse the repository at this point in the history
…e scenarios (#1099)
  • Loading branch information
imbcmdth authored Apr 27, 2017
1 parent d533971 commit 4daa28f
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 160 deletions.
174 changes: 122 additions & 52 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import Decrypter from './decrypter-worker';

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 Down Expand Up @@ -64,10 +72,7 @@ const objectChanged = function(a, b) {
*/
const parseCodecs = function(codecs) {
let result = {
codecCount: 0,
videoCodec: null,
videoObjectTypeIndicator: null,
audioProfile: null
codecCount: 0
};
let parsed;

Expand Down Expand Up @@ -104,6 +109,53 @@ export const mapLegacyAvcCodecs_ = function(codecString) {
});
};

/**
* 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;
};

/**
* Calculates the MIME type strings for a working configuration of
* SourceBuffers to play variant streams in a master playlist. If
Expand All @@ -119,74 +171,92 @@ export const mapLegacyAvcCodecs_ = function(codecString) {
* @private
*/
export const mimeTypesForPlaylist_ = function(master, media) {
let container = 'mp2t';
let codecs = {
videoCodec: 'avc1',
videoObjectTypeIndicator: '.4d400d',
audioProfile: '2'
};
let audioGroup = [];
let mediaAttributes;
let previousGroup = null;
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 an error
// Not enough information
return [];
}
// An initialization segment means the media playlists is an iframe
// playlist or is using the mp4 container. We don't currently
// support iframe playlists, so assume this is signalling mp4
// fragments.
// the existence check for segments can be removed once
// https://github.com/videojs/m3u8-parser/issues/8 is closed
if (media.segments && media.segments.length && media.segments[0].map) {
container = 'mp4';

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) {
if (!audioGroup[groupId].uri) {
isMuxed = true;
break;
}
}
}
}

// if the codecs were explicitly specified, use them instead of the
// defaults
mediaAttributes = media.attributes || {};
if (mediaAttributes.CODECS) {
let parsedCodecs = parseCodecs(mediaAttributes.CODECS);
// 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) {
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;
}

Object.keys(parsedCodecs).forEach((key) => {
codecs[key] = parsedCodecs[key] || codecs[key];
});
// Generate the final codec strings from the codec object generated above
let codecStrings = {};

if (codecInfo.videoCodec) {
codecStrings.video = `${codecInfo.videoCodec}${codecInfo.videoObjectTypeIndicator}`;
}

if (master.mediaGroups.AUDIO) {
audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO];
if (codecInfo.audioProfile) {
codecStrings.audio = `mp4a.40.${codecInfo.audioProfile}`;
}

// if audio could be muxed or unmuxed, use mime types appropriate
// for both scenarios
for (let groupId in audioGroup) {
if (previousGroup && (!!audioGroup[groupId].uri !== !!previousGroup.uri)) {
// one source buffer with muxed video and audio and another for
// the alternate audio
// 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 [
'video/' + container + '; codecs="' +
codecs.videoCodec + codecs.videoObjectTypeIndicator + ', mp4a.40.' + codecs.audioProfile + '"',
'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"'
justVideo,
justAudio
];
}
previousGroup = audioGroup[groupId];
// 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 all video and audio is unmuxed, use two single-codec mime
// types
if (previousGroup && previousGroup.uri) {

// If there is ano video codec at all, always just return a single
// audio/<container> mime-type
if (!codecStrings.video) {
return [
'video/' + container + '; codecs="' +
codecs.videoCodec + codecs.videoObjectTypeIndicator + '"',
'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"'
justAudio
];
}

// all video and audio are muxed, use a dual-codec mime type
// When not using separate audio media groups, audio and video is
// *always* muxed
return [
'video/' + container + '; codecs="' +
codecs.videoCodec + codecs.videoObjectTypeIndicator +
', mp4a.40.' + codecs.audioProfile + '"'
bothVideoAudio
];
};

Expand Down
Loading

0 comments on commit 4daa28f

Please sign in to comment.