Skip to content

Commit

Permalink
Add track restrictions to Player.
Browse files Browse the repository at this point in the history
Now the application can restrict the kinds of tracks that the Player
will play.  This is done through player.configure().  This also
allows restricting the playable tracks based on the EME key status.

Closes #326
Closes #327

Change-Id: I88210ece6fd1db886c49d4599fbe5814d394132d
  • Loading branch information
TheModMaker committed Apr 15, 2016
1 parent a8d12ba commit e1d834f
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 52 deletions.
12 changes: 11 additions & 1 deletion externs/shaka/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ shakaExtern.GetSegmentReferenceFunction;
* width: (number|undefined),
* height: (number|undefined),
* kind: (string|undefined),
* keyId: ?string
* keyId: ?string,
* allowedByApplication: boolean,
* allowedByKeySystem: boolean
* }}
*
* @description
Expand Down Expand Up @@ -325,6 +327,14 @@ shakaExtern.GetSegmentReferenceFunction;
* The stream's key ID as a lowercase hex string. This key ID identifies the
* encryption key that the browser (key system) can use to decrypt the
* stream.
* @property {boolean} allowedByApplication
* <i>Defaults to true.</i><br>
* Set by the Player to indicate whether the stream is allowed to be played
* by the application.
* @property {boolean} allowedByKeySystem
* <i>Defaults to true.</i><br>
* Set by the Player to indicate whether the stream is allowed to be played
* by the key system.
*
* @exportDoc
*/
Expand Down
53 changes: 52 additions & 1 deletion externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,53 @@ shakaExtern.Stats;
shakaExtern.Track;


/**
* @typedef {{
* minWidth: number,
* maxWidth: number,
* minHeight: number,
* maxHeight: number,
* minPixels: number,
* maxPixels: number,
*
* minAudioBandwidth: number,
* maxAudioBandwidth: number,
* minVideoBandwidth: number,
* maxVideoBandwidth: number
* }}
*
* @description
* An object describing application restrictions on what tracks can play. All
* restrictions must be fulfilled for a track to be playable. If a track does
* not meet the restrictions, it will not appear in the track list and it will
* not be played.
*
* @property {number} minWidth
* The minimum width of a video track, in pixels.
* @property {number} maxWidth
* The maximum width of a video track, in pixels.
* @property {number} minHeight
* The minimum height of a video track, in pixels.
* @property {number} maxHeight
* The maximum height of a video track, in pixels.
* @property {number} minPixels
* The minimum number of total pixels in a video track (i.e. width * height).
* @property {number} maxPixels
* The maximum number of total pixels in a video track (i.e. width * height).
*
* @property {number} minAudioBandwidth
* The minimum bandwidth of an audio track, in bit/sec.
* @property {number} maxAudioBandwidth
* The maximum bandwidth of an audio track, in bit/sec.
* @property {number} minVideoBandwidth
* The minimum bandwidth of a video track, in bit/sec.
* @property {number} maxVideoBandwidth
* The maximum bandwidth of a video track, in bit/sec.
* @exportDoc
*/
shakaExtern.Restrictions;


/**
* @typedef {{
* manifest: Object.<string, boolean>,
Expand Down Expand Up @@ -323,7 +370,8 @@ shakaExtern.AbrConfiguration;
* streaming: shakaExtern.StreamingConfiguration,
* abr: shakaExtern.AbrConfiguration,
* preferredAudioLanguage: string,
* preferredTextLanguage: string
* preferredTextLanguage: string,
* restrictions: shakaExtern.Restrictions
* }}
*
* @property {shakaExtern.DrmConfiguration} drm
Expand All @@ -345,6 +393,9 @@ shakaExtern.AbrConfiguration;
* the text track will be shown.
* Changing this during playback will cause the language selection algorithm
* to run again, and may change the active text track.
* @property {shakaExtern.Restrictions} restrictions
* The application restrictions to apply to the tracks. The track must
* meet all the restrictions to be playable.
* @exportDoc
*/
shakaExtern.PlayerConfiguration;
7 changes: 5 additions & 2 deletions lib/abr/simple_abr_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,10 @@ shaka.abr.SimpleAbrManager.prototype.chooseVideoStream_ = function() {
* @private
*/
shaka.abr.SimpleAbrManager.sortStreamsByBandwidth_ = function(streamSet) {
return streamSet.streams.slice(0).sort(
function(s1, s2) { return s1.bandwidth - s2.bandwidth; });
return streamSet.streams.slice(0)
.filter(function(s) {
return s.allowedByApplication && s.allowedByKeySystem;
})
.sort(function(s1, s2) { return s1.bandwidth - s2.bandwidth; });
};

4 changes: 3 additions & 1 deletion lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,9 @@ shaka.dash.DashParser.prototype.parseRepresentation_ = function(
width: context.representation.width,
height: context.representation.height,
kind: kind,
keyId: keyId
keyId: keyId,
allowedByApplication: true,
allowedByKeySystem: true
};
};

Expand Down
169 changes: 152 additions & 17 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,14 +533,21 @@ shaka.Player.prototype.applyConfig_ = function(
if (this.streamingEngine_) {
this.streamingEngine_.configure(this.config_.streaming);

// Need to apply the restrictions to every period.
this.manifest_.periods.forEach(this.applyRestrictions_.bind(this));

// If the languages have changed, then choose streams again.
var period = this.streamingEngine_.getCurrentPeriod();
// This will disable AbrManager but it will be enabled again below.
var chosen = this.chooseStreams_(period);
// Get the active streams to check if it is restricted. If it is, then
// AbrManager will make a new choice and we will apply it below.
var activeStreams = this.streamingEngine_.getActiveStreams();

for (var kind in chosen) {
if ((kind == 'audio' && audioLangChanged) ||
(kind == 'text' && textLangChanged)) {
(kind == 'text' && textLangChanged) ||
(!activeStreams[kind].allowedByApplication)) {
if (this.switchingPeriods_) {
this.deferredSwitches_[kind] = chosen[kind];
} else {
Expand Down Expand Up @@ -729,18 +736,22 @@ shaka.Player.prototype.getTracks = function() {
return period.streamSets
.map(function(streamSet) {
var activeStream = activeStreams[streamSet.type];
return streamSet.streams.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
};
});
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
};
});
})
.reduce(Functional.collapseArrays, []);
};
Expand Down Expand Up @@ -773,6 +784,13 @@ shaka.Player.prototype.selectTrack = function(track, opt_clearBuffer) {
return;
}

// Double check that the track is allowed to be played.
if (!stream.allowedByApplication || !stream.allowedByKeySystem) {
shaka.log.error('Unable to switch to track with id "' + track.id +
'" because it is restricted.');
return;
}

// Add an entry to the history.
this.switchHistory_.push({
timestamp: Date.now() / 1000,
Expand Down Expand Up @@ -920,7 +938,9 @@ shaka.Player.prototype.addTextTrack = function(
codecs: opt_codec || '',
bandwidth: 0,
kind: kind,
keyId: null
keyId: null,
allowedByApplication: true,
allowedByKeySystem: true
};
/** @type {shakaExtern.StreamSet} */
var streamSet = {
Expand Down Expand Up @@ -1160,7 +1180,19 @@ shaka.Player.prototype.defaultConfig_ = function() {
shaka.abr.EwmaBandwidthEstimator.DEFAULT_ESTIMATE
},
preferredAudioLanguage: '',
preferredTextLanguage: ''
preferredTextLanguage: '',
restrictions: {
minWidth: 0,
maxWidth: Number.POSITIVE_INFINITY,
minHeight: 0,
maxHeight: Number.POSITIVE_INFINITY,
minPixels: 0,
maxPixels: Number.POSITIVE_INFINITY,
minAudioBandwidth: 0,
maxAudioBandwidth: Number.POSITIVE_INFINITY,
minVideoBandwidth: 0,
maxVideoBandwidth: Number.POSITIVE_INFINITY
}
};
};

Expand All @@ -1180,6 +1212,49 @@ shaka.Player.prototype.defaultAdvancedDrmConfig_ = function() {
};


/**
* @param {shakaExtern.Period} period
* @private
*/
shaka.Player.prototype.applyRestrictions_ = function(period) {
var restrictions = this.config_.restrictions;
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.height < restrictions.minHeight ||
stream.height > restrictions.maxHeight ||
(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;
});
});

if (tracksChanged)
this.onTracksChanged_();

// We may be playing a restricted stream; but the calling code will handle it.
};


/**
* @param {shakaExtern.Period} period
* @private
Expand Down Expand Up @@ -1247,7 +1322,14 @@ shaka.Player.prototype.filterPeriod_ = function(period) {
}
}

if (period.streamSets.length == 0) {
this.applyRestrictions_(period);

var hasPlayableStreamSets = period.streamSets.some(function(streamSet) {
return streamSet.streams.some(function(stream) {
return stream.allowedByKeySystem && stream.allowedByApplication;
});
});
if (!hasPlayableStreamSets) {
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.UNPLAYABLE_PERIOD));
Expand Down Expand Up @@ -1367,6 +1449,19 @@ shaka.Player.prototype.chooseStreams_ = function(period) {
}.bind(this));
}.bind(this));

// If there are no unrestricted streams, issue an error.
var hasPlayableStreams = shaka.util.MapUtils.values(streamSetsByType)
.some(function(streamSet) {
return streamSet.streams.some(function(stream) {
return stream.allowedByApplication && stream.allowedByKeySystem;
});
});
if (!hasPlayableStreams) {
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.ALL_STREAMS_RESTRICTED));
}

var chosen = this.config_.abr.manager.chooseStreams(streamSetsByType);

// AbrManager does not choose text tracks, so use the first stream if it
Expand Down Expand Up @@ -1581,5 +1676,45 @@ shaka.Player.prototype.onVideoError_ = function(event) {
* @private
*/
shaka.Player.prototype.onKeyStatus_ = function(keyStatusMap) {
// TODO: use key status information
goog.asserts.assert(this.streamingEngine_, 'Should have been initialized.');
var allowedStatuses = ['usable', 'status-pending', 'output-downscaled'];
var period = this.streamingEngine_.getCurrentPeriod();
var tracksChanged = false;

period.streamSets.forEach(function(streamSet) {
streamSet.streams.forEach(function(stream) {
var originalAllowed = stream.allowedByKeySystem;

if (stream.keyId && stream.keyId in keyStatusMap) {
// Only update the status if it is in the map. If it is not in the map,
// then it was not updated and so it's status has not changed.
stream.allowedByKeySystem =
allowedStatuses.indexOf(keyStatusMap[stream.keyId]) >= 0;
}

if (originalAllowed != stream.allowedByKeySystem)
tracksChanged = true;
});
});

// This will disable AbrManager.
var chosen = this.chooseStreams_(period);
if (this.config_.abr.enabled && !this.switchingPeriods_)
this.config_.abr.manager.enable();

// If we are playing a restricted track, we will need to switch.
var activeStreams = this.streamingEngine_.getActiveStreams();
for (var kind in chosen) {
if (!activeStreams[kind].allowedByKeySystem) {
if (this.switchingPeriods_) {
this.deferredSwitches_[kind] = chosen[kind];
} else {
this.streamingEngine_.switch(kind, chosen[kind],
/* clearBuffer */ true);
}
}
}

if (tracksChanged)
this.onTracksChanged_();
};
7 changes: 7 additions & 0 deletions lib/util/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ shaka.util.Error.Code = {
*/
'UNPLAYABLE_PERIOD': 4011,

/**
* There exist some playable streams; however because of restrictions imposed
* by the application or the key system, all of them are not useable.
*/
'ALL_STREAMS_RESTRICTED': 4012,


/**
* The StreamingEngine appended a segment but the SourceBuffer is empty, or
* the StreamingEngine removed all segments and the SourceBuffer is
Expand Down

0 comments on commit e1d834f

Please sign in to comment.