shaka-player
Version:
DASH/EME video player library
855 lines (753 loc) • 28.8 kB
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.
*/
describe('StreamingEngine', function() {
/** @const */
var ContentType = shaka.util.ManifestParserUtils.ContentType;
/** @const */
var Util = shaka.test.Util;
var metadata;
var generators;
/** @type {!shaka.util.EventManager} */
var eventManager;
/** @type {!HTMLVideoElement} */
var video;
var timeline;
/** @type {!shaka.media.Playhead} */
var playhead;
/** @type {shakaExtern.StreamingConfiguration} */
var config;
var netEngine;
/** @type {!MediaSource} */
var mediaSource;
/** @type {!shaka.media.MediaSourceEngine} */
var mediaSourceEngine;
/** @type {!shaka.media.StreamingEngine} */
var streamingEngine;
/** @type {shakaExtern.Variant} */
var variant1;
/** @type {shakaExtern.Variant} */
var variant2;
/** @type {shakaExtern.Manifest} */
var manifest;
/** @type {!jasmine.Spy} */
var onBuffering;
/** @type {!jasmine.Spy} */
var onChooseStreams;
/** @type {!jasmine.Spy} */
var onCanSwitch;
/** @type {!jasmine.Spy} */
var onError;
/** @type {!jasmine.Spy} */
var onEvent;
/** @type {!jasmine.Spy} */
var onInitialStreamsSetup;
/** @type {!jasmine.Spy} */
var onStartupComplete;
beforeAll(function() {
video = /** @type {!HTMLVideoElement} */ (document.createElement('video'));
video.width = 600;
video.height = 400;
video.muted = true;
document.body.appendChild(video);
metadata = shaka.test.TestScheme.DATA['sintel'];
generators = {};
});
beforeEach(function(done) {
// shakaExtern.StreamingConfiguration
config = {
rebufferingGoal: 2,
bufferingGoal: 5,
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
failureCallback: function() {},
bufferBehind: 15,
ignoreTextStreamFailures: false,
useRelativeCueTimestamps: false,
startAtSegmentBoundary: false,
smallGapLimit: 0.5,
jumpLargeGaps: false,
durationBackoff: 1
};
onChooseStreams = jasmine.createSpy('onChooseStreams');
onCanSwitch = jasmine.createSpy('onCanSwitch');
onInitialStreamsSetup = jasmine.createSpy('onInitialStreamsSetup');
onStartupComplete = jasmine.createSpy('onStartupComplete');
onError = jasmine.createSpy('onError');
onError.and.callFake(fail);
onEvent = jasmine.createSpy('onEvent');
eventManager = new shaka.util.EventManager();
setupMediaSource().catch(fail).then(done);
});
afterEach(function(done) {
streamingEngine.destroy().then(function() {
video.removeAttribute('src');
video.load();
return Promise.all([
mediaSourceEngine.destroy(),
playhead.destroy(),
eventManager.destroy()
]);
}).then(function() {
// Work-around: allow the Tizen media pipeline to cool down.
// Without this, Tizen's pipeline seems to hang in subsequent tests.
// TODO: file a bug on Tizen
return Util.delay(0.1);
}).catch(fail).then(done);
});
afterAll(function() {
document.body.removeChild(video);
});
// Setup MediaSource and MediaSourceEngine.
function setupMediaSource() {
mediaSource = new MediaSource();
video.src = window.URL.createObjectURL(mediaSource);
var p = new shaka.util.PublicPromise();
var onMediaSourceOpen = function() {
eventManager.unlisten(mediaSource, 'sourceopen');
mediaSource.duration = 0;
mediaSourceEngine = new shaka.media.MediaSourceEngine(
video, mediaSource, null);
p.resolve();
};
eventManager.listen(mediaSource, 'sourceopen', onMediaSourceOpen);
return p;
}
function setupVod() {
return Promise.all([
createVodStreamGenerator(metadata.audio, ContentType.AUDIO),
createVodStreamGenerator(metadata.video, ContentType.VIDEO)
]).then(function() {
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
0 /* segmentAvailabilityStart */,
60 /* segmentAvailabilityEnd */,
60 /* presentationDuration */,
false /* isLive */);
setupNetworkingEngine(
0 /* firstPeriodStartTime */,
30 /* secondPeriodStartTime */,
60 /* presentationDuration */,
{ audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration });
setupManifest(
0 /* firstPeriodStartTime */,
30 /* secondPeriodStartTime */,
60 /* presentationDuration */);
setupPlayhead();
createStreamingEngine();
});
}
function setupLive() {
return Promise.all([
createLiveStreamGenerator(
metadata.audio, ContentType.AUDIO,
20 /* timeShiftBufferDepth */),
createLiveStreamGenerator(
metadata.video, ContentType.VIDEO,
20 /* timeShiftBufferDepth */)
]).then(function() {
// The generator's AST is set to 295 seconds in the past, so the live-edge
// is at 295 - 10 seconds.
// -10 to account for maxSegmentDuration.
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
275 - 10 /* segmentAvailabilityStart */,
295 - 10 /* segmentAvailabilityEnd */,
Infinity /* presentationDuration */,
true /* isLive */);
setupNetworkingEngine(
0 /* firstPeriodStartTime */,
300 /* secondPeriodStartTime */,
Infinity /* presentationDuration */,
{ audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration });
setupManifest(
0 /* firstPeriodStartTime */,
300 /* secondPeriodStartTime */,
Infinity /* presentationDuration */);
setupPlayhead();
createStreamingEngine();
});
}
function createVodStreamGenerator(metadata, type) {
var generator = new shaka.test.DashVodStreamGenerator(
metadata.initSegmentUri,
metadata.mvhdOffset,
metadata.segmentUri,
metadata.tfdtOffset,
metadata.segmentDuration,
metadata.presentationTimeOffset);
generators[type] = generator;
return generator.init();
}
function createLiveStreamGenerator(metadata, type, timeShiftBufferDepth) {
// Set the generator's AST to 295 seconds in the past so the
// StreamingEngine begins streaming close to the end of the first Period.
var now = Date.now() / 1000;
var generator = new shaka.test.DashLiveStreamGenerator(
metadata.initSegmentUri,
metadata.mvhdOffset,
metadata.segmentUri,
metadata.tfdtOffset,
metadata.segmentDuration,
metadata.presentationTimeOffset,
now - 295 /* broadcastStartTime */,
now - 295 /* availabilityStartTime */,
timeShiftBufferDepth);
generators[type] = generator;
return generator.init();
}
function setupNetworkingEngine(firstPeriodStartTime, secondPeriodStartTime,
presentationDuration, segmentDurations) {
var periodStartTimes = [firstPeriodStartTime, secondPeriodStartTime];
var boundsCheckPosition =
shaka.test.StreamingEngineUtil.boundsCheckPosition.bind(
null, periodStartTimes, presentationDuration, segmentDurations);
var getNumSegments =
shaka.test.StreamingEngineUtil.getNumSegments.bind(
null, periodStartTimes, presentationDuration, segmentDurations);
// Create the fake NetworkingEngine. Note: the StreamingEngine should never
// request a segment that does not exist.
netEngine = shaka.test.StreamingEngineUtil.createFakeNetworkingEngine(
// Init segment generator:
function(type, periodNumber) {
expect(periodNumber).toBeLessThan(periodStartTimes.length + 1);
var wallClockTime = Date.now() / 1000;
var segment = generators[type].getInitSegment(wallClockTime);
expect(segment).not.toBeNull();
return segment;
},
// Media segment generator:
function(type, periodNumber, position) {
expect(boundsCheckPosition(type, periodNumber, position))
.not.toBeNull();
// Compute the total number of segments in all Periods before the
// |periodNumber|'th one.
var numPriorSegments = 0;
for (var n = 1; n < periodNumber; ++n)
numPriorSegments += getNumSegments(type, n);
var wallClockTime = Date.now() / 1000;
var segment = generators[type].getSegment(
position, numPriorSegments, wallClockTime);
expect(segment).not.toBeNull();
return segment;
});
}
function setupPlayhead() {
onBuffering = jasmine.createSpy('onBuffering');
var onSeek = function() { streamingEngine.seeked(); };
playhead = new shaka.media.Playhead(
/** @type {!HTMLVideoElement} */(video),
/** @type {shakaExtern.Manifest} */ (manifest),
config,
null /* startTime */,
onSeek,
shaka.test.Util.spyFunc(onEvent));
}
function setupManifest(
firstPeriodStartTime, secondPeriodStartTime, presentationDuration) {
manifest = shaka.test.StreamingEngineUtil.createManifest(
[firstPeriodStartTime, secondPeriodStartTime], presentationDuration,
{ audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration });
manifest.presentationTimeline =
/** @type {!shaka.media.PresentationTimeline} */ (timeline);
manifest.minBufferTime = 2;
// Create InitSegmentReferences.
function makeUris(uri) { return function() { return [uri]; }; }
manifest.periods[0].variants[0].audio.initSegmentReference =
new shaka.media.InitSegmentReference(makeUris('1_audio_init'), 0, null);
manifest.periods[0].variants[0].video.initSegmentReference =
new shaka.media.InitSegmentReference(makeUris('1_video_init'), 0, null);
manifest.periods[1].variants[0].audio.initSegmentReference =
new shaka.media.InitSegmentReference(makeUris('2_audio_init'), 0, null);
manifest.periods[1].variants[0].video.initSegmentReference =
new shaka.media.InitSegmentReference(makeUris('2_video_init'), 0, null);
variant1 = manifest.periods[0].variants[0];
variant2 = manifest.periods[1].variants[0];
}
function createStreamingEngine() {
var playerInterface = {
playhead: playhead,
mediaSourceEngine: mediaSourceEngine,
netEngine: /** @type {!shaka.net.NetworkingEngine} */(netEngine),
onChooseStreams: Util.spyFunc(onChooseStreams),
onCanSwitch: Util.spyFunc(onCanSwitch),
onError: Util.spyFunc(onError),
onEvent: Util.spyFunc(onEvent),
onManifestUpdate: function() {},
onSegmentAppended: playhead.onSegmentAppended.bind(playhead),
onInitialStreamsSetup: Util.spyFunc(onInitialStreamsSetup),
onStartupComplete: Util.spyFunc(onStartupComplete)
};
streamingEngine = new shaka.media.StreamingEngine(
/** @type {shakaExtern.Manifest} */(manifest), playerInterface);
streamingEngine.configure(config);
}
describe('VOD', function() {
beforeEach(function(done) {
setupVod().catch(fail).then(done);
});
it('plays', function(done) {
onStartupComplete.and.callFake(function() {
video.play();
});
var onEnded = function() {
// Some browsers may not end at exactly 60 seconds.
expect(Math.round(video.currentTime)).toBe(60);
done();
};
eventManager.listen(video, 'ended', onEnded);
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
streamingEngine.init().catch(function(error) {
fail(error);
done();
});
});
it('plays at high playback rates', function(done) {
var startupComplete = false;
onStartupComplete.and.callFake(function() {
startupComplete = true;
video.play();
});
onBuffering.and.callFake(function(buffering) {
if (!buffering) {
expect(startupComplete).toBeTruthy();
video.playbackRate = 10;
}
});
var onEnded = function() {
// Some browsers may not end at exactly 60 seconds.
expect(Math.round(video.currentTime)).toBe(60);
done();
};
eventManager.listen(video, 'ended', onEnded);
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
streamingEngine.init().catch(function(error) {
fail(error);
done();
});
});
it('can handle buffered seeks', function(done) {
onStartupComplete.and.callFake(function() {
video.play();
});
// After 35 seconds seek back 10 seconds into the first Period.
var onTimeUpdate = function() {
if (video.currentTime >= 35) {
eventManager.unlisten(video, 'timeupdate');
video.currentTime = 25;
}
};
eventManager.listen(video, 'timeupdate', onTimeUpdate);
var onEnded = function() {
// Some browsers may not end at exactly 60 seconds.
expect(Math.round(video.currentTime)).toBe(60);
done();
};
eventManager.listen(video, 'ended', onEnded);
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
streamingEngine.init().catch(function(error) {
fail(error);
done();
});
});
it('can handle unbuffered seeks', function(done) {
onStartupComplete.and.callFake(function() {
video.play();
});
// After 20 seconds seek 10 seconds into the second Period.
var onTimeUpdate = function() {
if (video.currentTime >= 20) {
eventManager.unlisten(video, 'timeupdate');
video.currentTime = 40;
}
};
eventManager.listen(video, 'timeupdate', onTimeUpdate);
var onEnded = function() {
// Some browsers may not end at exactly 60 seconds.
expect(Math.round(video.currentTime)).toBe(60);
done();
};
eventManager.listen(video, 'ended', onEnded);
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
streamingEngine.init().catch(function(error) {
fail(error);
done();
});
});
});
describe('Live', function() {
var slideSegmentAvailabilityWindow;
beforeEach(function(done) {
setupLive().then(function() {
slideSegmentAvailabilityWindow = window.setInterval(function() {
timeline.segmentAvailabilityStart++;
timeline.segmentAvailabilityEnd++;
}, 1000);
}).catch(fail).then(done);
});
afterEach(function() {
window.clearInterval(slideSegmentAvailabilityWindow);
});
it('plays through Period transition', function(done) {
onStartupComplete.and.callFake(function() {
// firstSegmentNumber =
// [(segmentAvailabilityEnd - rebufferingGoal) / segmentDuration] + 1
// Then -1 to account for drift safe buffering.
var segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
netEngine.expectRequest('1_video_28', segmentType);
netEngine.expectRequest('1_audio_28', segmentType);
video.play();
});
var onTimeUpdate = function() {
if (video.currentTime >= 305) {
// We've played through the Period transition!
eventManager.unlisten(video, 'timeupdate');
done();
}
};
eventManager.listen(video, 'timeupdate', onTimeUpdate);
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
streamingEngine.init().catch(function(error) {
fail(error);
done();
});
});
it('can handle seeks ahead of availability window',
function(done) {
onStartupComplete.and.callFake(function() {
video.play();
// Use setTimeout to ensure the playhead has performed it's initial
// seeking.
setTimeout(function() {
// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityEnd + 120;
// Wait until the repositioning is complete so we don't
// immediately hit this case.
setTimeout(function() {
var onTimeUpdate = function() {
if (video.currentTime >= 305) {
// We've played through the Period transition!
eventManager.unlisten(video, 'timeupdate');
done();
}
};
eventManager.listen(video, 'timeupdate', onTimeUpdate);
}, 1000);
}, 50);
});
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
streamingEngine.init().catch(function(error) {
fail(error);
done();
});
});
it('can handle seeks behind availability window', function(done) {
onStartupComplete.and.callFake(function() {
video.play();
// Use setTimeout to ensure the playhead has performed it's initial
// seeking.
setTimeout(function() {
// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityStart - 120;
expect(video.currentTime).toBeGreaterThan(0);
}, 50);
});
var seekCount = 0;
eventManager.listen(video, 'seeking', function() {
seekCount++;
});
var onTimeUpdate = function() {
if (video.currentTime >= 305) {
// We've played through the Period transition!
eventManager.unlisten(video, 'timeupdate');
// We are playing close to the beginning of the availability window.
// We should be playing smoothly and not seeking repeatedly as we fall
// outside the window.
//
// Expected seeks:
// 1. seek to live stream start time during startup
// 2. explicit seek in the test to get outside the window
// 3. Playhead seeks to force us back inside the window
// 4. (maybe) seek if there is a gap at the period boundary
// 5. (maybe) seek to flush a pipeline stall
expect(seekCount).toBeGreaterThan(2);
expect(seekCount).toBeLessThan(6);
done();
}
};
eventManager.listen(video, 'timeupdate', onTimeUpdate);
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
streamingEngine.init().catch(function(error) {
fail(error);
done();
});
});
});
// This tests gaps created by missing segments.
// TODO: Consider also adding tests for missing frames.
describe('gap jumping', function() {
it('jumps small gaps at the beginning', function(done) {
config.smallGapLimit = 5;
setupGappyContent(/* gapAtStart */ 1, /* dropSegment */ false)
.then(function() {
onStartupComplete.and.callFake(function() {
expect(video.buffered.length).toBeGreaterThan(0);
expect(video.buffered.start(0)).toBeCloseTo(1);
video.play();
});
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
return streamingEngine.init();
}).then(function() {
return waitForTime(5);
}).catch(fail).then(done);
});
it('jumps large gaps at the beginning', function(done) {
config.smallGapLimit = 1;
config.jumpLargeGaps = true;
setupGappyContent(/* gapAtStart */ 5, /* dropSegment */ false)
.then(function() {
onStartupComplete.and.callFake(function() {
expect(video.buffered.length).toBeGreaterThan(0);
expect(video.buffered.start(0)).toBeCloseTo(5);
video.play();
});
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
return streamingEngine.init();
}).then(function() {
return waitForTime(8);
}).catch(fail).then(done);
});
it('jumps small gaps in the middle', function(done) {
config.smallGapLimit = 20;
setupGappyContent(/* gapAtStart */ 0, /* dropSegment */ true)
.then(function() {
onStartupComplete.and.callFake(function() {
video.currentTime = 8;
video.play();
});
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
return streamingEngine.init();
}).then(function() {
return waitForTime(23);
}).then(function() {
// Should be close enough to still have the gap buffered.
expect(video.buffered.length).toBe(2);
expect(onEvent).not.toHaveBeenCalled();
}).catch(fail).then(done);
});
it('jumps large gaps in the middle', function(done) {
config.jumpLargeGaps = true;
setupGappyContent(/* gapAtStart */ 0, /* dropSegment */ true)
.then(function() {
onStartupComplete.and.callFake(function() {
video.currentTime = 8;
video.play();
});
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
return streamingEngine.init();
}).then(function() {
return waitForTime(23);
}).then(function() {
// Should be close enough to still have the gap buffered.
expect(video.buffered.length).toBe(2);
expect(onEvent).toHaveBeenCalled();
}).catch(fail).then(done);
});
it('won\'t jump large gaps with preventDefault()', function(done) {
config.jumpLargeGaps = true;
setupGappyContent(/* gapAtStart */ 0, /* dropSegment */ true)
.then(function() {
onStartupComplete.and.callFake(function() {
video.currentTime = 8;
video.play();
});
onEvent.and.callFake(function(event) {
event.preventDefault();
shaka.test.Util.delay(5).then(function() {
// IE/Edge somehow plays inside the gap. Just make sure we
// don't jump the gap.
expect(video.currentTime).toBeLessThan(20);
done();
})
.catch(done.fail);
});
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
return streamingEngine.init();
}).catch(done.fail);
});
/**
* @param {number} gapAtStart The gap to introduce before start, in seconds.
* @param {boolean} dropSegment Whether to drop a segment in the middle.
* @return {!Promise}
*/
function setupGappyContent(gapAtStart, dropSegment) {
// This uses "normal" stream generators and networking engine. The only
// difference is the segments are removed from the manifest. The segments
// should not be downloaded.
return Promise.all([
createVodStreamGenerator(metadata.audio, ContentType.AUDIO),
createVodStreamGenerator(metadata.video, ContentType.VIDEO)
]).then(function() {
timeline =
shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
0 /* segmentAvailabilityStart */,
30 /* segmentAvailabilityEnd */,
30 /* presentationDuration */,
false /* isLive */);
setupNetworkingEngine(
0 /* firstPeriodStartTime */,
30 /* secondPeriodStartTime */,
30 /* presentationDuration */,
{ audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration });
manifest = setupGappyManifest(gapAtStart, dropSegment);
variant1 = manifest.periods[0].variants[0];
setupPlayhead();
createStreamingEngine();
});
}
/**
* TODO: Consolidate with StreamingEngineUtils.createManifest?
* @param {number} gapAtStart
* @param {boolean} dropSegment
* @return {shakaExtern.Manifest}
*/
function setupGappyManifest(gapAtStart, dropSegment) {
/**
* @param {string} type
* @return {!shaka.media.SegmentIndex}
*/
function createIndex(type) {
var d = metadata[type].segmentDuration;
var refs = [];
var i = 1;
var time = gapAtStart;
while (time < 30) {
var end = time + d;
// Make segment 1 longer to make the manifest continuous, despite the
// dropped segment.
if (i == 1 && dropSegment)
end += d;
var getUris = (function(i) {
// The times in the media are based on the URL; so to drop a
// segment, we change the URL.
if (i >= 2 && dropSegment) i++;
return ['1_' + type + '_' + i];
}.bind(null, i));
refs.push(
new shaka.media.SegmentReference(i, time, end, getUris, 0, null));
i++;
time = end;
}
return new shaka.media.SegmentIndex(refs);
}
function createInit(type) {
var getUris = function() {
return ['1_' + type + '_init'];
};
return new shaka.media.InitSegmentReference(getUris, 0, null);
}
var videoIndex = createIndex('video');
var audioIndex = createIndex('audio');
return {
presentationTimeline: timeline,
offlineSessionIds: [],
minBufferTime: 2,
periods: [{
startTime: 0,
textStreams: [],
variants: [{
id: 1,
video: {
id: 2,
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: videoIndex.find.bind(videoIndex),
getSegmentReference: videoIndex.get.bind(videoIndex),
initSegmentReference: createInit('video'),
// Normally PTO adjusts the segment time backwards; so to make the
// segment appear in the future, use a negative.
presentationTimeOffset: -gapAtStart,
mimeType: 'video/mp4',
codecs: 'avc1.42c01e',
bandwidth: 5000000,
width: 600,
height: 400,
type: shaka.util.ManifestParserUtils.ContentType.VIDEO
},
audio: {
id: 3,
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: audioIndex.find.bind(audioIndex),
getSegmentReference: audioIndex.get.bind(audioIndex),
initSegmentReference: createInit('audio'),
presentationTimeOffset: -gapAtStart,
mimeType: 'audio/mp4',
codecs: 'mp4a.40.2',
bandwidth: 192000,
type: shaka.util.ManifestParserUtils.ContentType.AUDIO
}
}]
}]
};
}
/**
* @param {number} time
* @return {!Promise}
*/
function waitForTime(time) {
var p = new shaka.util.PublicPromise();
var onTimeUpdate = function() {
if (video.currentTime >= time) {
p.resolve();
}
};
eventManager.listen(video, 'timeupdate', onTimeUpdate);
var timeout = shaka.test.Util.delay(30).then(function() {
throw 'Timeout waiting for time';
});
return Promise.race([p, timeout]);
}
});
/**
* Choose streams for the given period.
*
* @param {shakaExtern.Period} period
* @return {!Object.<string, !shakaExtern.Stream>}
*/
function defaultOnChooseStreams(period) {
if (period == manifest.periods[0]) {
return { variant: variant1, text: null };
} else if (period == manifest.periods[1]) {
return { variant: variant2, text: null };
} else {
throw new Error();
}
}
});