Files
shaka-player/lib/util/stream_utils.js
T
Joey Parrish 4c9ee5db76 Allow text parsers to change during playback
Text parsers can easily be changed during playback, unlike the
browser's media decoders.  Whenever we change text tracks,
reinitialize the text parser.

This also removes restrictions on text tracks of different types
when we filter manifests.

Closes #571

Change-Id: I3c243dde951cd04554c2fa1be2c8ebf06988791b
2016-11-21 17:50:08 +00:00

367 lines
12 KiB
JavaScript

/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.StreamUtils');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.MediaSourceEngine');
goog.require('shaka.util.Functional');
goog.require('shaka.util.LanguageUtils');
/**
* @param {shakaExtern.Period} period
* @param {shakaExtern.Restrictions} restrictions
* Configured restrictions from the user.
* @param {{width: number, height: number}} maxHwRes
* The maximum resolution the hardware can handle.
* This is applied separately from user restrictions because the setting
* should not be easily replaced by the user's configuration.
* @return {boolean} Whether the tracks changed.
*/
shaka.util.StreamUtils.applyRestrictions =
function(period, restrictions, maxHwRes) {
var tracksChanged = false;
period.streamSets.forEach(function(streamSet) {
streamSet.streams.forEach(function(stream) {
var originalAllowed = stream.allowedByApplication;
stream.allowedByApplication = true;
if (streamSet.type == 'video') {
if (stream.width < restrictions.minWidth ||
stream.width > restrictions.maxWidth ||
stream.width > maxHwRes.width ||
stream.height < restrictions.minHeight ||
stream.height > restrictions.maxHeight ||
stream.height > maxHwRes.height ||
(stream.width * stream.height) < restrictions.minPixels ||
(stream.width * stream.height) > restrictions.maxPixels ||
stream.bandwidth < restrictions.minVideoBandwidth ||
stream.bandwidth > restrictions.maxVideoBandwidth) {
stream.allowedByApplication = false;
}
} else if (streamSet.type == 'audio') {
if (stream.bandwidth < restrictions.minAudioBandwidth ||
stream.bandwidth > restrictions.maxAudioBandwidth) {
stream.allowedByApplication = false;
}
}
if (originalAllowed != stream.allowedByApplication)
tracksChanged = true;
});
});
return tracksChanged;
};
/**
* Alters the given Period to filter out any unplayable streams.
*
* @param {shaka.media.DrmEngine} drmEngine
* @param {!Object.<string, shakaExtern.Stream>} activeStreams
* @param {shakaExtern.Period} period
*/
shaka.util.StreamUtils.filterPeriod = function(
drmEngine, activeStreams, period) {
var keySystem = '';
var drmSupportedMimeTypes = null;
if (drmEngine && drmEngine.initialized()) {
keySystem = drmEngine.keySystem();
drmSupportedMimeTypes = drmEngine.getSupportedTypes();
}
for (var i = 0; i < period.streamSets.length; ++i) {
var streamSet = period.streamSets[i];
if (keySystem) {
// A key system has been selected.
// Remove streamSets which can only be used with other key systems.
// Note that drmInfos == [] means unencrypted.
var match = streamSet.drmInfos.length == 0 ||
streamSet.drmInfos.some(function(drmInfo) {
return drmInfo.keySystem == keySystem; });
if (!match) {
shaka.log.debug('Dropping StreamSet, can\'t be used with ' + keySystem,
streamSet);
period.streamSets.splice(i, 1);
--i;
continue;
}
}
var activeStream = activeStreams[streamSet.type];
for (var j = 0; j < streamSet.streams.length; ++j) {
var stream = streamSet.streams[j];
var fullMimeType = stream.mimeType;
if (stream.codecs) {
fullMimeType += '; codecs="' + stream.codecs + '"';
}
if (!shaka.media.MediaSourceEngine.isTypeSupported(fullMimeType)) {
// Remove streams that cannot be played by the platform.
streamSet.streams.splice(j, 1);
--j;
continue;
}
if (drmSupportedMimeTypes && stream.encrypted &&
drmSupportedMimeTypes.indexOf(fullMimeType) < 0) {
// Remove encrypted streams that cannot be handled by the key system.
streamSet.streams.splice(j, 1);
--j;
continue;
}
if (activeStream && streamSet.type != 'text') {
// Check that the basic mime types and basic codecs match.
// For example, we can't adapt between WebM and MP4,
// nor can we adapt between mp4a.* to ec-3.
// We can switch between text types on the fly,
// so don't run this check on text.
if (stream.mimeType != activeStream.mimeType ||
stream.codecs.split('.')[0] != activeStream.codecs.split('.')[0]) {
streamSet.streams.splice(j, 1);
--j;
continue;
}
}
}
if (streamSet.streams.length == 0) {
period.streamSets.splice(i, 1);
--i;
}
}
};
/**
* Gets an array of Track objects for the given Period
*
* @param {shakaExtern.Period} period
* @param {Object.<string, shakaExtern.Stream>} activeStreams
* @return {!Array.<shakaExtern.Track>}
*/
shaka.util.StreamUtils.getTracks = function(period, activeStreams) {
// Convert each stream into a track and squash them into one array.
var Functional = shaka.util.Functional;
return period.streamSets
.map(function(streamSet) {
var activeStream = activeStreams ? activeStreams[streamSet.type] : null;
return streamSet.streams
.filter(function(stream) {
return stream.allowedByApplication && stream.allowedByKeySystem;
})
.map(function(stream) {
return {
id: stream.id,
active: activeStream == stream,
type: streamSet.type,
bandwidth: stream.bandwidth,
language: streamSet.language,
kind: stream.kind || null,
width: stream.width || null,
height: stream.height || null,
frameRate: stream.frameRate || undefined,
codecs: stream.codecs || null
};
});
})
.reduce(Functional.collapseArrays, []);
};
/**
* Find the stream and stream set for the given track.
*
* @param {shakaExtern.Period} period
* @param {shakaExtern.Track} track
* @return {?{stream: shakaExtern.Stream, streamSet: shakaExtern.StreamSet}}
*/
shaka.util.StreamUtils.findStreamForTrack = function(period, track) {
for (var i = 0; i < period.streamSets.length; i++) {
var streamSet = period.streamSets[i];
for (var j = 0; j < streamSet.streams.length; j++) {
var stream = streamSet.streams[j];
if (stream.id == track.id)
return {stream: stream, streamSet: streamSet};
}
}
return null;
};
/**
* Determines if the given stream set has any playable streams.
* @param {shakaExtern.StreamSet} streamSet
* @return {boolean}
*/
shaka.util.StreamUtils.hasPlayableStreams = function(streamSet) {
return streamSet.streams.some(function(stream) {
return stream.allowedByApplication && stream.allowedByKeySystem;
});
};
/**
* Chooses a stream set of each type according to the given config.
*
* @param {shakaExtern.Period} period
* @param {shakaExtern.PlayerConfiguration} config
* @param {!Object=} opt_languageMatches
* @return {!Object.<string, shakaExtern.StreamSet>}
*/
shaka.util.StreamUtils.chooseStreamSets = function(
period, config, opt_languageMatches) {
var LanguageUtils = shaka.util.LanguageUtils;
var hasPlayableStreams = shaka.util.StreamUtils.hasPlayableStreams;
var StreamUtils = shaka.util.StreamUtils;
// Choose the first stream set listed as the default.
/** @type {!Object.<string, shakaExtern.StreamSet>} */
var streamSetsByType = {};
period.streamSets.forEach(function(set) {
if (!hasPlayableStreams(set) || set.type in streamSetsByType) return;
streamSetsByType[set.type] = set;
});
// Pick video set with highest top resolution, break ties
// by selecting one with lower average bandwidth
var highestResolution = 0;
period.streamSets.forEach(function(set) {
if (hasPlayableStreams(set) && set.type == 'video') {
var resolution = StreamUtils.getHighestResolution(set);
if (resolution > highestResolution) {
highestResolution = resolution;
streamSetsByType['video'] = set;
} else if (resolution == highestResolution) {
if (StreamUtils.getAvgBandwidth(set) <
StreamUtils.getAvgBandwidth(streamSetsByType['video'])) {
streamSetsByType['video'] = set;
}
}
}
});
// Then if there are primary stream sets, override the default.
period.streamSets.forEach(function(set) {
if (hasPlayableStreams(set) && set.primary) {
// if both sets are primary, choose one with lower
// average bandwidth
if (streamSetsByType[set.type].primary) {
if (StreamUtils.getAvgBandwidth(set) <
StreamUtils.getAvgBandwidth(streamSetsByType[set.type])) {
streamSetsByType[set.type] = set;
}
} else {
streamSetsByType[set.type] = set;
}
}
});
// Finally, choose based on language preference. Favor exact matches, then
// base matches, finally different subtags. Execute in reverse order so
// the later steps override the previous ones.
[LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY,
LanguageUtils.MatchType.BASE_LANGUAGE_OKAY,
LanguageUtils.MatchType.EXACT]
.forEach(function(matchType) {
period.streamSets.forEach(function(set) {
if (!hasPlayableStreams(set))
return;
/** @type {string} */
var pref;
if (set.type == 'audio')
pref = config.preferredAudioLanguage;
else if (set.type == 'text')
pref = config.preferredTextLanguage;
if (pref) {
pref = LanguageUtils.normalize(pref);
var lang = LanguageUtils.normalize(set.language);
if (LanguageUtils.match(matchType, pref, lang)) {
// If this audio stream has the same language as a previous
// match, only choose it if it uses less bandwidth.
if (set.language == streamSetsByType[set.type].language) {
if (StreamUtils.getAvgBandwidth(set) <
StreamUtils.getAvgBandwidth(streamSetsByType[set.type])) {
streamSetsByType[set.type] = set;
}
} else {
streamSetsByType[set.type] = set;
}
if (opt_languageMatches)
opt_languageMatches[set.type] = true;
}
}
});
});
return streamSetsByType;
};
/**
* Computes average bandwidth across all streams of a stream set.
* Assumes a stream set of audio/video type, where all streams have
* bandwidth.
*
* @param {shakaExtern.StreamSet} streamSet
* @return {number}
*/
shaka.util.StreamUtils.getAvgBandwidth = function(streamSet) {
var bandwidthSum = 0;
// to make sure we don't end up trying to divide by 0
if (!streamSet || streamSet.streams.length < 1) return bandwidthSum;
streamSet.streams.forEach(function(stream) {
bandwidthSum += stream.bandwidth;
});
return bandwidthSum / streamSet.streams.length;
};
/**
* Loops through all the streams in a StreamSet and returns the value
* of the highest resolution found across all streams.
* Assumes a valid video StreamSet.
*
* @param {shakaExtern.StreamSet} streamSet
* @return {number}
*/
shaka.util.StreamUtils.getHighestResolution = function(streamSet) {
var highestRes = 0;
if (!streamSet) return highestRes;
streamSet.streams.forEach(function(stream) {
if (stream.height > highestRes)
highestRes = stream.height;
});
return highestRes;
};