Skip to content

Commit

Permalink
DASH trick mode support
Browse files Browse the repository at this point in the history
Parses DASH trick mode tracks and puts the extra trick mode Stream
into the manifest.  StreamingEngine can now use this info to optimize
streaming during trick play mode.

Includes:
 - a new demo asset with a trick mode track
 - updates to tests (we now require at least one audio or video stream
   and we require bandwidth attributes on them)
 - updates to the parser's trickmode test
 - a new StreamingEngine test

Closes #538

Change-Id: Id38264ca64bc7905a5c33a269269741cfd12dd4d
  • Loading branch information
joeyparrish committed Jan 5, 2017
1 parent e093ab2 commit c645169
Show file tree
Hide file tree
Showing 13 changed files with 423 additions and 80 deletions.
17 changes: 17 additions & 0 deletions demo/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ shakaAssets.Feature = {
PSSH: 'embedded PSSH',
MULTIKEY: 'multiple keys',
MULTIPERIOD: 'multiple Periods',
TRICK_MODE: 'special trick mode track',

SUBTITLES: 'subtitles',
CAPTIONS: 'captions',
Expand Down Expand Up @@ -342,6 +343,22 @@ shakaAssets.testAssets = [
shakaAssets.Feature.WEBVTT
]
},
{
name: 'Sintel w/ trick mode (MP4 only, 720p)',
manifestUri: '//storage.googleapis.com/shaka-demo-assets/sintel-trickplay/dash.mpd', // gjslint: disable=110

encoder: shakaAssets.Encoder.SHAKA_PACKAGER,
source: shakaAssets.Source.SHAKA,
drm: [],
features: [
shakaAssets.Feature.HIGH_DEFINITION,
shakaAssets.Feature.MP4,
shakaAssets.Feature.SEGMENT_BASE,
shakaAssets.Feature.SUBTITLES,
shakaAssets.Feature.TRICK_MODE,
shakaAssets.Feature.WEBVTT
]
},
{
name: 'Sintel 4k (WebM only)',
manifestUri: '//storage.googleapis.com/shaka-demo-assets/sintel-webm-only/dash.mpd', // gjslint: disable=110
Expand Down
6 changes: 5 additions & 1 deletion externs/shaka/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ shakaExtern.GetSegmentReferenceFunction;
* keyId: ?string,
* language: string,
* type: string,
* primary: boolean
* primary: boolean,
* trickModeVideo: ?shakaExtern.Stream
* }}
*
* @description
Expand Down Expand Up @@ -373,6 +374,9 @@ shakaExtern.GetSegmentReferenceFunction;
* True indicates that the player should prefer this Stream over others
* in the same Period. However, the player may use another
* Stream to meet application preferences.
* @property {?shakaExtern.Stream} trickModeVideo
* <i>Video streams only.</i> <br>
* An alternate video stream to use for trick mode playback.
*
* @exportDoc
*/
Expand Down
137 changes: 91 additions & 46 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ shaka.dash.DashParser.PeriodInfo;
* main: boolean,
* streams: !Array.<shakaExtern.Stream>,
* drmInfos: !Array.<shakaExtern.DrmInfo>,
* containsInband: boolean
* containsInband: boolean,
* trickModeFor: ?string
* }}
*
* @description
Expand All @@ -256,6 +257,10 @@ shaka.dash.DashParser.PeriodInfo;
* The DRM info for the AdaptationSet.
* @property {boolean} containsInband
* Signals whether AdaptationSet has inband content indicator on it.
* @property {?string} trickModeFor
* If non-null, this AdaptationInfo represents trick mode tracks. This
* property is the ID of the normal AdaptationSet these tracks should be
* associated with.
*/
shaka.dash.DashParser.AdaptationInfo;

Expand Down Expand Up @@ -700,53 +705,71 @@ shaka.dash.DashParser.prototype.parsePeriod_ = function(
.map(this.parseAdaptationSet_.bind(this, context))
.filter(shaka.util.Functional.isNotNull);

if (adaptationSets.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_PERIOD);
}
var normalAdaptationSets = adaptationSets
.filter(function(as) { return !as.trickModeFor; });

var trickModeAdaptationSets = adaptationSets
.filter(function(as) { return as.trickModeFor; });

// Attach trick mode tracks to normal tracks.
trickModeAdaptationSets.forEach(function(trickModeSet) {
// There may be multiple trick mode streams, but we do not currently
// support that. Just choose one.
var trickModeVideo = trickModeSet.streams[0];
var targetId = trickModeSet.trickModeFor;
normalAdaptationSets.forEach(function(normalSet) {
if (normalSet.id == targetId) {
normalSet.streams.forEach(function(stream) {
stream.trickModeVideo = trickModeVideo;
});
}
});
});

// see if any adaptation set has emsg indicator on it.
// See if any adaptation set has emsg indicator on it.
// If it does, we'll register a response filter later.
for (var i = 0; i < adaptationSets.length; i++) {
if (adaptationSets[i].containsInband) {
for (var i = 0; i < normalAdaptationSets.length; i++) {
if (normalAdaptationSets[i].containsInband) {
periodInfo.containsInband = true;
}
}
var videoSets = this.getSetsOfType_(adaptationSets, 'video');
var audioSets = this.getSetsOfType_(adaptationSets, 'audio');
var variants = [];

// TODO: Limit number of combinations. Come up with a heuristic
// to decide which audio tracks to combine with which video tracks.
for (var i = 0; i < audioSets.length; i++) {
for (var j = 0; j < videoSets.length; j++) {
variants.push.apply(variants,
this.createVariants_(audioSets[i], videoSets[j]));
}
var videoSets = this.getSetsOfType_(normalAdaptationSets, 'video');
var audioSets = this.getSetsOfType_(normalAdaptationSets, 'audio');

if (!videoSets.length && !audioSets.length) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_PERIOD);
}

// In case of audio-only or video-only content, we create an array of one item
// containing a null. This way, the double-loop works for all kinds of
// content.
if (!audioSets.length) {
// Video only variants
for (var i = 0; i < videoSets.length; i++) {
variants.push.apply(variants,
this.createVariants_(null, videoSets[i]));
}
audioSets = [null];
}

if (!videoSets.length) {
// Audio only variants
for (var i = 0; i < audioSets.length; i++) {
variants.push.apply(variants,
this.createVariants_(audioSets[i], null));
videoSets = [null];
}

// TODO: Limit number of combinations. Come up with a heuristic
// to decide which audio tracks to combine with which video tracks.
var variants = [];
for (var i = 0; i < audioSets.length; i++) {
for (var j = 0; j < videoSets.length; j++) {
var audioSet = audioSets[i];
var videoSet = videoSets[j];
this.createVariants_(audioSet, videoSet, variants);
}
}

var textSets = this.getSetsOfType_(adaptationSets, 'text');
var textSets = this.getSetsOfType_(normalAdaptationSets, 'text');
var textStreams = [];
for (var i = 0; i < textSets.length; i++) {
textStreams.push.apply(textStreams, textSets[i].streams);
}

return {
startTime: periodInfo.start,
textStreams: textStreams,
Expand Down Expand Up @@ -774,14 +797,26 @@ shaka.dash.DashParser.prototype.getSetsOfType_ = function(
*
* @param {?shaka.dash.DashParser.AdaptationInfo} audio
* @param {?shaka.dash.DashParser.AdaptationInfo} video
* @return {!Array.<!shakaExtern.Variant>}
* @param {!Array.<shakaExtern.Variant>} variants New variants are pushed onto
* this array.
* @private
*/
shaka.dash.DashParser.prototype.createVariants_ = function(audio, video) {
var variants = [];
shaka.dash.DashParser.prototype.createVariants_ =
function(audio, video, variants) {
// Since both audio and video are of the same type, this assertion will catch
// certain mistakes at runtime that the compiler would miss.
goog.asserts.assert(!audio || audio.contentType == 'audio',
'Audio parameter mistmatch!');
goog.asserts.assert(!video || video.contentType == 'video',
'Video parameter mistmatch!');

/** @type {number} */
var bandwidth;
/** @type {shakaExtern.Variant} */
var variant;

if (!audio && !video) {
return variants;
return;
} else if (audio && video) {
// Audio+video variants
var DrmEngine = shaka.media.DrmEngine;
Expand All @@ -791,13 +826,21 @@ shaka.dash.DashParser.prototype.createVariants_ = function(audio, video) {

for (var i = 0; i < audio.streams.length; i++) {
for (var j = 0; j < video.streams.length; j++) {
var variant = {
// Explicit cast, followed by assertion. These should both be defined
// in the case of DASH, but the type of Stream.bandwidth allows for
// undefined in order to support HLS.
bandwidth = /** @type {number} */(
video.streams[j].bandwidth +
audio.streams[i].bandwidth);
goog.asserts.assert(bandwidth,
'Bandwidth must be defined and non-zero!');
variant = {
id: this.globalId_++,
language: audio.language,
primary: audio.main || video.main,
audio: audio.streams[i],
video: video.streams[j],
bandwidth: video.streams[j].bandwidth + audio.streams[i].bandwidth,
bandwidth: bandwidth,
drmInfos: drmInfos,
allowedByApplication: true,
allowedByKeySystem: true
Expand All @@ -811,13 +854,19 @@ shaka.dash.DashParser.prototype.createVariants_ = function(audio, video) {
// Audio or video only variants
var set = audio || video;
for (var i = 0; i < set.streams.length; i++) {
var variant = {
// Explicit cast, followed by assertion. These should both be defined
// in the case of DASH, but the type allows for undefined in order to
// support HLS.
bandwidth = /** @type {number} */(set.streams[i].bandwidth);
goog.asserts.assert(bandwidth,
'Bandwidth must be defined and non-zero!');
variant = {
id: this.globalId_++,
language: set.language || '',
primary: set.main,
audio: audio ? set.streams[i] : null,
video: video ? set.streams[i] : null,
bandwidth: set.streams[i].bandwidth,
bandwidth: bandwidth,
drmInfos: set.drmInfos,
allowedByApplication: true,
allowedByKeySystem: true
Expand All @@ -826,8 +875,6 @@ shaka.dash.DashParser.prototype.createVariants_ = function(audio, video) {
variants.push(variant);
}
}

return variants;
};


Expand Down Expand Up @@ -884,10 +931,6 @@ shaka.dash.DashParser.prototype.parseAdaptationSet_ = function(context, elem) {
trickModeFor = prop.getAttribute('value');
}
});
if (trickModeFor != null) {
// Ignore trick mode tracks until we support them fully.
return null;
}

var contentProtectionElems = XmlUtils.findChildren(elem, 'ContentProtection');
var contentProtection = shaka.dash.ContentProtection.parseFromAdaptationSet(
Expand Down Expand Up @@ -927,7 +970,8 @@ shaka.dash.DashParser.prototype.parseAdaptationSet_ = function(context, elem) {
main: main,
streams: streams,
drmInfos: contentProtection.drmInfos,
containsInband: containsInband
containsInband: containsInband,
trickModeFor: trickModeFor
};
};

Expand Down Expand Up @@ -1025,7 +1069,8 @@ shaka.dash.DashParser.prototype.parseRepresentation_ = function(
keyId: keyId,
language: language,
type: context.adaptationSet.contentType,
primary: isPrimary
primary: isPrimary,
trickModeVideo: null
};
};

Expand Down
Loading

0 comments on commit c645169

Please sign in to comment.