3f8b9462 by David LaPalomento

Merge pull request #459 from dmlap/long-term-seekable-start

Long term seekable start
2 parents e02911f3 8f15253b
......@@ -152,6 +152,11 @@
// initialize the loader state
loader.state = 'HAVE_NOTHING';
// track the time that has expired from the live window
// this allows the seekable start range to be calculated even if
// all segments with timing information have expired
this.expired_ = 0;
// capture the prototype dispose function
dispose = this.dispose;
......@@ -346,7 +351,52 @@
* @param update {object} the updated media playlist object
*/
PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
var outdated, i, segment;
outdated = this.media_;
this.media_ = this.master.playlists[update.uri];
if (!outdated) {
return;
}
// try using precise timing from first segment of the updated
// playlist
if (update.segments.length) {
if (update.segments[0].start !== undefined) {
this.expired_ = update.segments[0].start;
return;
} else if (update.segments[0].end !== undefined) {
this.expired_ = update.segments[0].end - update.segments[0].duration;
return;
}
}
// calculate expired by walking the outdated playlist
i = update.mediaSequence - outdated.mediaSequence - 1;
for (; i >= 0; i--) {
segment = outdated.segments[i];
if (!segment) {
// we missed information on this segment completely between
// playlist updates so we'll have to take an educated guess
// once we begin buffering again, any error we introduce can
// be corrected
this.expired_ += outdated.targetDuration || 10;
continue;
}
if (segment.end !== undefined) {
this.expired_ = segment.end;
return;
}
if (segment.start !== undefined) {
this.expired_ = segment.start + segment.duration;
return;
}
this.expired_ += segment.duration;
}
};
/**
......@@ -457,8 +507,8 @@
if (i === endIndex) {
// We haven't found a segment but we did hit a known end point
// so fallback to "Algorithm Jon" - try to interpolate the segment
// index based on the known span of the timeline we are dealing with
// so fallback to interpolating between the segment index
// based on the known span of the timeline we are dealing with
// and the number of segments inside that span
return startIndex + Math.floor(
((originalTime - knownStart) / (knownEnd - knownStart)) *
......@@ -481,9 +531,13 @@
// We haven't found a segment so load the first one
return 0;
} else {
// We known nothing so use "Algorithm A" - walk from the front
// of the playlist naively subtracking durations until we find
// a segment that contains time and return it
// We known nothing so walk from the front of the playlist,
// subtracting durations until we find a segment that contains
// time and return it
time = time - this.expired_;
if (time < 0) {
return -1;
}
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
time -= segment.duration || targetDuration;
......
......@@ -187,7 +187,7 @@ videojs.HlsHandler.prototype.src = function(src) {
}.bind(this));
this.playlists.on('loadedplaylist', function() {
var updatedPlaylist = this.playlists.media();
var updatedPlaylist = this.playlists.media(), seekable;
if (!updatedPlaylist) {
// select the initial variant
......@@ -196,6 +196,14 @@ videojs.HlsHandler.prototype.src = function(src) {
}
this.updateDuration(this.playlists.media());
// update seekable
seekable = this.seekable();
if (this.duration() === Infinity &&
seekable.length !== 0) {
this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
}
oldMediaPlaylist = updatedPlaylist;
}.bind(this));
......@@ -291,7 +299,6 @@ videojs.Hls.bufferedAdditions_ = function(original, update) {
return result;
};
var parseCodecs = function(codecs) {
var result = {
codecCount: 0,
......@@ -312,6 +319,7 @@ var parseCodecs = function(codecs) {
return result;
};
/**
* Blacklist playlists that are known to be codec or
* stream-incompatible with the SourceBuffer configuration. For
......@@ -445,15 +453,15 @@ videojs.HlsHandler.prototype.play = function() {
// if the viewer has paused and we fell out of the live window,
// seek forward to the earliest available position
if (this.duration() === Infinity) {
if (this.tech_.currentTime() < this.tech_.seekable().start(0)) {
this.tech_.setCurrentTime(this.tech_.seekable().start(0));
if (this.tech_.currentTime() < this.seekable().start(0)) {
this.tech_.setCurrentTime(this.seekable().start(0));
}
}
};
videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) {
var
buffered = this.findCurrentBuffered_();
buffered = this.findBufferedRange_();
if (!(this.playlists && this.playlists.media())) {
// return immediately if the metadata is not ready yet
......@@ -501,7 +509,7 @@ videojs.HlsHandler.prototype.duration = function() {
};
videojs.HlsHandler.prototype.seekable = function() {
var media;
var media, seekable;
if (!this.playlists) {
return videojs.createTimeRanges();
......@@ -511,7 +519,25 @@ videojs.HlsHandler.prototype.seekable = function() {
return videojs.createTimeRanges();
}
return videojs.Hls.Playlist.seekable(media);
seekable = videojs.Hls.Playlist.seekable(media);
if (seekable.length === 0) {
return seekable;
}
// if the seekable start is zero, it may be because the player has
// been paused for a long time and stopped buffering. in that case,
// fall back to the playlist loader's running estimate of expired
// time
if (seekable.start(0) === 0) {
return videojs.createTimeRanges([[
this.playlists.expired_,
this.playlists.expired_ + seekable.end(0)
]]);
}
// seekable has been calculated based on buffering video data so it
// can be returned directly
return seekable;
};
/**
......@@ -522,15 +548,10 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) {
newDuration = videojs.Hls.Playlist.duration(playlist),
setDuration = function() {
this.mediaSource.duration = newDuration;
// update seekable
if (seekable.length !== 0 && newDuration === Infinity) {
this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
}
this.tech_.trigger('durationchange');
this.mediaSource.removeEventListener('sourceopen', setDuration);
}.bind(this),
seekable = this.seekable();
}.bind(this);
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
......@@ -539,10 +560,6 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) {
this.mediaSource.addEventListener('sourceopen', setDuration);
} else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
this.mediaSource.duration = newDuration;
// update seekable
if (seekable.length !== 0 && newDuration === Infinity) {
this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
}
this.tech_.trigger('durationchange');
}
}
......@@ -745,43 +762,63 @@ videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() {
this.tech_.off('waiting', this.drainBuffer);
};
/**
* Attempts to find the buffered TimeRange where playback is currently
* happening. Returns a new TimeRange with one or zero ranges.
*/
videojs.HlsHandler.prototype.findCurrentBuffered_ = function() {
var filterBufferedRanges = function(predicate) {
return function(time) {
var
ranges,
i,
ranges = [],
tech = this.tech_,
// !!The order of the next two lines is important!!
// !!The order of the next two assignments is important!!
// `currentTime` must be equal-to or greater-than the start of the
// buffered range. Flash executes out-of-process so, every value can
// change behind the scenes from line-to-line. By reading `currentTime`
// after `buffered`, we ensure that it is always a current or later
// value during playback.
buffered = tech.buffered(),
currentTime = tech.currentTime();
buffered = tech.buffered();
if (time === undefined) {
time = tech.currentTime();
}
if (buffered && buffered.length) {
// Search for a range containing the play-head
for (i = 0; i < buffered.length; i++) {
if (buffered.start(i) - TIME_FUDGE_FACTOR <= currentTime &&
buffered.end(i) + TIME_FUDGE_FACTOR >= currentTime) {
ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i));
ranges.indexOf = i;
return ranges;
if (predicate(buffered.start(i), buffered.end(i), time)) {
ranges.push([buffered.start(i), buffered.end(i)]);
}
}
}
// Return an empty range if no ranges exist
ranges = videojs.createTimeRanges();
ranges.indexOf = -1;
return ranges;
return videojs.createTimeRanges(ranges);
};
};
/**
* Attempts to find the buffered TimeRange that contains the specified
* time, or where playback is currently happening if no specific time
* is specified.
* @param time (optional) {number} the time to filter on. Defaults to
* currentTime.
* @return a new TimeRanges object.
*/
videojs.HlsHandler.prototype.findBufferedRange_ = filterBufferedRanges(function(start, end, time) {
return start - TIME_FUDGE_FACTOR <= time &&
end + TIME_FUDGE_FACTOR >= time;
});
/**
* Returns the TimeRanges that begin at or later than the specified
* time.
* @param time (optional) {number} the time to filter on. Defaults to
* currentTime.
* @return a new TimeRanges object.
*/
videojs.HlsHandler.prototype.findNextBufferedRange_ = filterBufferedRanges(function(start, end, time) {
return start - TIME_FUDGE_FACTOR >= time;
});
/**
* Determines whether there is enough video data currently in the buffer
* and downloads a new segment if the buffered time is less than the goal.
* @param seekToTime (optional) {number} the offset into the downloaded segment
......@@ -791,7 +828,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
var
tech = this.tech_,
currentTime = tech.currentTime(),
currentBuffered = this.findCurrentBuffered_(),
currentBuffered = this.findBufferedRange_(),
currentBufferedEnd = 0,
bufferedTime = 0,
segment,
......@@ -1025,7 +1062,7 @@ videojs.HlsHandler.prototype.drainBuffer = function(event) {
segIv,
segmentTimestampOffset = 0,
hasBufferedContent = (this.tech_.buffered().length !== 0),
currentBuffered = this.findCurrentBuffered_(),
currentBuffered = this.findBufferedRange_(),
outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
// if the buffer is empty or the source buffer hasn't been created
......@@ -1132,6 +1169,7 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
playlist,
currentMediaIndex,
currentBuffered,
seekable,
timelineUpdates;
this.pendingSegment_ = null;
......@@ -1144,7 +1182,7 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
playlist = this.playlists.media();
segments = playlist.segments;
currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence);
currentBuffered = this.findCurrentBuffered_();
currentBuffered = this.findBufferedRange_();
// if we switched renditions don't try to add segment timeline
// information to the playlist
......@@ -1156,9 +1194,25 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
// added by the media processing
segment = playlist.segments[currentMediaIndex];
// when seeking to the beginning of the seekable range, it's
// possible that imprecise timing information may cause the seek to
// end up earlier than the start of the range
// in that case, seek again
seekable = this.seekable();
if (this.tech_.seeking() &&
currentBuffered.length === 0) {
if (seekable.length &&
this.tech_.currentTime() < seekable.start(0)) {
var next = this.findNextBufferedRange_();
if (next.length) {
videojs.log('tried seeking to', this.tech_.currentTime(), 'but that was too early, retrying at', next.start(0));
this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR);
}
}
}
timelineUpdates = videojs.Hls.bufferedAdditions_(segmentInfo.buffered,
this.tech_.buffered());
timelineUpdates.forEach(function (update) {
if (segment) {
if (update.end !== undefined) {
......
......@@ -53,6 +53,17 @@
strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
});
test('starts with no expired time', function() {
var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
equal(loader.expired_,
0,
'zero seconds expired');
});
test('requests the initial playlist immediately', function() {
new videojs.Hls.PlaylistLoader('master.m3u8');
strictEqual(requests.length, 1, 'made a request');
......@@ -166,6 +177,125 @@
strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
});
test('increments expired seconds after a segment is removed', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n' +
'#EXTINF:10,\n' +
'4.ts\n');
equal(loader.expired_, 10, 'expired one segment');
});
test('increments expired seconds after a discontinuity', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
equal(loader.expired_, 10, 'expired one segment');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:2\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
equal(loader.expired_, 13, 'no expirations after the discontinuity yet');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'3.ts\n');
equal(loader.expired_, 17, 'tracked expiration across the discontinuity');
});
test('tracks expired seconds properly when two discontinuities expire at once', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:6,\n' +
'2.ts\n' +
'#EXTINF:7,\n' +
'3.ts\n');
clock.tick(10 * 1000);
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
'#EXTINF:7,\n' +
'3.ts\n');
equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities');
});
test('estimates expired if an entire window elapses between live playlist updates', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n');
clock.tick(10 * 1000);
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:4\n' +
'#EXTINF:6,\n' +
'4.ts\n' +
'#EXTINF:7,\n' +
'5.ts\n');
equal(loader.expired_,
4 + 5 + (2 * 10),
'made a very rough estimate of expired time');
});
test('emits an error when an initial playlist request fails', function() {
var
errors = [],
......@@ -672,7 +802,7 @@
equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5');
});
test('accounts for expired time when calculating media index', function() {
test('accounts for non-zero starting segment time when calculating media index', function() {
var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
requests.shift().respond(200, null,
'#EXTM3U\n' +
......@@ -693,6 +823,53 @@
equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment');
});
test('prefers precise segment timing when tracking expired time', function() {
var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
// setup the loader with an "imprecise" value as if it had been
// accumulating segment durations as they expire
loader.expired_ = 160;
// annotate the first segment with a start time
// this number would be coming from the Source Buffer in practice
loader.media().segments[0].start = 150;
equal(loader.getMediaIndexForTime_(151), 0, 'prefers the value on the first segment');
clock.tick(10 * 1000); // trigger a playlist refresh
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1002\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
equal(loader.getMediaIndexForTime_(150 + 4 + 1), 0, 'tracks precise expired times');
});
test('accounts for expired time when calculating media index', function() {
var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
loader.expired_ = 150;
equal(loader.getMediaIndexForTime_(0), -1, 'expired content returns a negative index');
equal(loader.getMediaIndexForTime_(75), -1, 'expired content returns a negative index');
equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position');
equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment');
equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment');
equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment');
equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment');
});
test('does not misintrepret playlists missing newlines at the end', function() {
var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
requests.shift().respond(200, null,
......
......@@ -45,6 +45,12 @@
}
</style>
<script>
if (window.location.search === '?flash') {
videojs.options.techOrder = ['flash'];
}
</script>
</head>
<body>
<div class="info">
......@@ -64,6 +70,7 @@
type="application/x-mpegURL">
</video>
<section class="stats">
<div class="player-stats">
<h2>Player Stats</h2>
<dl>
<dt>Current Time:</dt>
......@@ -77,7 +84,21 @@
<dt>Measured Bitrate:</dt>
<dd class="measured-bitrate-stat">0 kbps</dd>
</dl>
<h3>Bitrate Switching</h3>
</div>
<div class="event-counts">
<h2>Event Counts</h2>
<dl>
<dt>Play:</dt>
<dd class="play-count">0</dd>
<dt>Playing:</dt>
<dd class="playing-count">0</dd>
<dt>Seeking:</dt>
<dd class="seeking-count">0</dd>
<dt>Seeked:</dt>
<dd class="seeked-count">0</dd>
</dl>
</div>
<h3 class="bitrate-switching">Bitrate Switching</h3>
<div class="switching-stats">
Once the player begins loading, you'll see information about the
operation of the adaptive quality switching here.
......@@ -90,7 +111,7 @@
<script>
videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf';
// initialize the player
var player = videojs('video');
var player = videojs('video').ready(function() {
// ------------
// Player Stats
......@@ -107,7 +128,7 @@
currentTimeStat.textContent = player.currentTime().toFixed(1);
});
player.on('progress', function() {
window.setInterval(function() {
var bufferedText = '', oldStart, oldEnd, i;
// buffered
......@@ -146,10 +167,40 @@
maximumFractionDigits: 1
}) + ' kbps';
}
}, 1000);
var trackEventCount = function(eventName, selector) {
var count = 0, element = document.querySelector(selector);
player.on(eventName, function() {
count++;
element.innerHTML = count;
});
};
trackEventCount('play', '.play-count');
trackEventCount('playing', '.playing-count');
trackEventCount('seeking', '.seeking-count');
trackEventCount('seeked', '.seeked-count');
videojs.Hls.displayStats(document.querySelector('.switching-stats'), player);
videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player);
});
// -----------
// Tech Switch
// -----------
var techSwitch = document.createElement('a');
techSwitch.className = 'tech-switch';
if (player.el().querySelector('video')) {
techSwitch.href = window.location.origin + window.location.pathname + '?flash';
techSwitch.appendChild(document.createTextNode('Switch to the Flash tech'));
} else {
techSwitch.href = window.location.origin + window.location.pathname;
techSwitch.appendChild(document.createTextNode('Stop forcing Flash'));
}
document.body.insertBefore(techSwitch, document.querySelector('.stats'));
</script>
</body>
</html>
......
.tech-switch {
padding: 8px 0 0;
display: block;
}
.player-stats, .event-counts {
box-sizing: border-box;
padding: 8px;
float: left;
width: 50%;
}
h3.bitrate-switching {
clear: both;
}
.axis text,
.cue text {
font: 12px sans-serif;
......
......@@ -549,11 +549,11 @@ test('finds the correct buffered region based on currentTime', function() {
standardXHRResponse(requests[1]);
player.currentTime(3);
clock.tick(1);
equal(player.tech_.hls.findCurrentBuffered_().end(0),
equal(player.tech_.hls.findBufferedRange_().end(0),
5, 'inside the first buffered region');
player.currentTime(6);
clock.tick(1);
equal(player.tech_.hls.findCurrentBuffered_().end(0),
equal(player.tech_.hls.findBufferedRange_().end(0),
12, 'inside the second buffered region');
});
......@@ -1636,6 +1636,41 @@ test('live playlist starts with correct currentTime value', function() {
'currentTime is updated at playback');
});
test('adjusts the seekable start based on the amount of expired live content', function() {
player.src({
src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests.shift());
// add timeline info to the playlist
player.tech_.hls.playlists.media().segments[1].end = 29.5;
// expired_ should be ignored if there is timeline information on
// the playlist
player.tech_.hls.playlists.expired_ = 172;
equal(player.seekable().start(0),
29.5 - 29,
'offset the seekable start');
});
test('estimates seekable ranges for live streams that have been paused for a long time', function() {
player.src({
src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests.shift());
player.tech_.hls.playlists.expired_ = 172;
equal(player.seekable().start(0),
player.tech_.hls.playlists.expired_,
'offset the seekable start');
});
test('resets the time to a seekable position when resuming a live stream ' +
'after a long break', function() {
var seekTarget;
......