diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa068cfe5..428f205bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@ CHANGELOG
=========
## HEAD (Unreleased)
-_(none)_
+* @dmlap implement seekable for live streams. Fix in-band metadata timing for live streams. ([view](https://github.com/videojs/videojs-contrib-hls/pull/295))
--------------------
diff --git a/Gruntfile.js b/Gruntfile.js
index f4e630045..f02910006 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -34,6 +34,7 @@ module.exports = function(grunt) {
'src/segment-parser.js',
'src/m3u8/m3u8-parser.js',
'src/xhr.js',
+ 'src/playlist.js',
'src/playlist-loader.js',
'node_modules/pkcs7/dist/pkcs7.unpad.js',
'src/decrypter.js'
diff --git a/example.html b/example.html
index 1d938d3b7..18b252ff9 100644
--- a/example.html
+++ b/example.html
@@ -28,13 +28,14 @@
+
-
+
diff --git a/src/aac-stream.js b/src/aac-stream.js
index 5bba5aac0..0285ecd0c 100644
--- a/src/aac-stream.js
+++ b/src/aac-stream.js
@@ -12,7 +12,6 @@ var
window.videojs.Hls.AacStream = function() {
var
next_pts, // :uint
- pts_offset, // :int
state, // :uint
pes_length, // :int
lastMetaPts,
@@ -32,7 +31,6 @@ window.videojs.Hls.AacStream = function() {
// (pts:uint):void
this.setTimeStampOffset = function(pts) {
- pts_offset = pts;
// keep track of the last time a metadata tag was written out
// set the initial value so metadata will be generated before any
@@ -42,7 +40,7 @@ window.videojs.Hls.AacStream = function() {
// (pts:uint, pes_size:int, dataAligned:Boolean):void
this.setNextTimeStamp = function(pts, pes_size, dataAligned) {
- next_pts = pts - pts_offset;
+ next_pts = pts;
pes_length = pes_size;
// If data is aligned, flush all internal buffers
diff --git a/src/h264-stream.js b/src/h264-stream.js
index a9e1b4427..dff147d6c 100644
--- a/src/h264-stream.js
+++ b/src/h264-stream.js
@@ -37,7 +37,6 @@
window.videojs.Hls.H264Stream = H264Stream = function() {
this._next_pts = 0; // :uint;
this._next_dts = 0; // :uint;
- this._pts_offset = 0; // :int
this._h264Frame = null; // :FlvTag
@@ -52,15 +51,13 @@
};
//(pts:uint):void
- H264Stream.prototype.setTimeStampOffset = function(pts) {
- this._pts_offset = pts;
- };
+ H264Stream.prototype.setTimeStampOffset = function() {};
//(pts:uint, dts:uint, dataAligned:Boolean):void
H264Stream.prototype.setNextTimeStamp = function(pts, dts, dataAligned) {
// We could end up with a DTS less than 0 here. We need to deal with that!
- this._next_pts = pts - this._pts_offset;
- this._next_dts = dts - this._pts_offset;
+ this._next_pts = pts;
+ this._next_dts = dts;
// If data is aligned, flush all internal buffers
if (dataAligned) {
diff --git a/src/m3u8/m3u8-parser.js b/src/m3u8/m3u8-parser.js
index 89ffb5b9d..2eb537c2b 100644
--- a/src/m3u8/m3u8-parser.js
+++ b/src/m3u8/m3u8-parser.js
@@ -210,6 +210,18 @@
this.trigger('data', event);
return;
}
+ match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
+ if (match) {
+ event = {
+ type: 'tag',
+ tagType: 'discontinuity-sequence'
+ };
+ if (match[1]) {
+ event.number = parseInt(match[1], 10);
+ }
+ this.trigger('data', event);
+ return;
+ }
match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line);
if (match) {
event = {
@@ -308,7 +320,7 @@
event.attributes = parseAttributes(match[1]);
// parse the IV string into a Uint32Array
if (event.attributes.IV) {
- if (event.attributes.IV.substring(0,2) === '0x') {
+ if (event.attributes.IV.substring(0,2) === '0x') {
event.attributes.IV = event.attributes.IV.substring(2);
}
@@ -409,6 +421,12 @@
message: 'defaulting media sequence to zero'
});
}
+ if (!('discontinuitySequence' in this.manifest)) {
+ this.manifest.discontinuitySequence = 0;
+ this.trigger('info', {
+ message: 'defaulting discontinuity sequence to zero'
+ });
+ }
if (entry.duration >= 0) {
currentUri.duration = entry.duration;
}
@@ -459,6 +477,15 @@
}
this.manifest.mediaSequence = entry.number;
},
+ 'discontinuity-sequence': function() {
+ if (!isFinite(entry.number)) {
+ this.trigger('warn', {
+ message: 'ignoring invalid discontinuity sequence: ' + entry.number
+ });
+ return;
+ }
+ this.manifest.discontinuitySequence = entry.number;
+ },
'playlist-type': function() {
if (!(/VOD|EVENT/).test(entry.playlistType)) {
this.trigger('warn', {
diff --git a/src/metadata-stream.js b/src/metadata-stream.js
index 61afaacef..c8757bd65 100644
--- a/src/metadata-stream.js
+++ b/src/metadata-stream.js
@@ -37,7 +37,8 @@
if (tag.data[i] === 0) {
// parse the text fields
tag.description = parseUtf8(tag.data, 1, i);
- tag.value = parseUtf8(tag.data, i + 1, tag.data.length);
+ // do not include the null terminator in the tag value
+ tag.value = parseUtf8(tag.data, i + 1, tag.data.length - 1);
break;
}
}
@@ -173,13 +174,6 @@
(tag.data[19]);
}
- // adjust the PTS values to align with the video and audio
- // streams
- if (this.timestampOffset) {
- tag.pts -= this.timestampOffset;
- tag.dts -= this.timestampOffset;
- }
-
// parse one or more ID3 frames
// http://id3.org/id3v2.3.0#ID3v2_frame_overview
do {
diff --git a/src/playlist-loader.js b/src/playlist-loader.js
index 7446a1a8e..0d6ae1090 100644
--- a/src/playlist-loader.js
+++ b/src/playlist-loader.js
@@ -1,12 +1,23 @@
/**
* A state machine that manages the loading, caching, and updating of
- * M3U8 playlists.
+ * M3U8 playlists. When tracking a live playlist, loaders will keep
+ * track of the duration of content that expired since the loader was
+ * initialized and when the current discontinuity sequence was
+ * encountered. A complete media timeline for a live playlist with
+ * expiring segments and discontinuities looks like this:
+ *
+ * |-- expiredPreDiscontinuity --|-- expiredPostDiscontinuity --|-- segments --|
+ *
+ * You can use these values to calculate how much time has elapsed
+ * since the stream began loading or how long it has been since the
+ * most recent discontinuity was encountered, for instance.
*/
(function(window, videojs) {
'use strict';
var
resolveUrl = videojs.Hls.resolveUrl,
xhr = videojs.Hls.xhr,
+ Playlist = videojs.Hls.Playlist,
/**
* Returns a new master playlist that is the result of merging an
@@ -51,66 +62,84 @@
var
loader = this,
dispose,
- media,
mediaUpdateTimeout,
request,
+ haveMetadata;
- haveMetadata = function(error, xhr, url) {
- var parser, refreshDelay, update;
+ PlaylistLoader.prototype.init.call(this);
- loader.setBandwidth(request || xhr);
+ if (!srcUrl) {
+ throw new Error('A non-empty playlist URL is required');
+ }
- // any in-flight request is now finished
- request = null;
+ // update the playlist loader's state in response to a new or
+ // updated playlist.
+ haveMetadata = function(error, xhr, url) {
+ var parser, refreshDelay, update;
- if (error) {
- loader.error = {
- status: xhr.status,
- message: 'HLS playlist request error at URL: ' + url,
- responseText: xhr.responseText,
- code: (xhr.status >= 500) ? 4 : 2
- };
- return loader.trigger('error');
- }
+ loader.setBandwidth(request || xhr);
- loader.state = 'HAVE_METADATA';
+ // any in-flight request is now finished
+ request = null;
- parser = new videojs.m3u8.Parser();
- parser.push(xhr.responseText);
- parser.end();
- parser.manifest.uri = url;
-
- // merge this playlist into the master
- update = updateMaster(loader.master, parser.manifest);
- refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
- if (update) {
- loader.master = update;
- media = loader.master.playlists[url];
- } else {
- // if the playlist is unchanged since the last reload,
- // try again after half the target duration
- refreshDelay /= 2;
- }
+ if (error) {
+ loader.error = {
+ status: xhr.status,
+ message: 'HLS playlist request error at URL: ' + url,
+ responseText: xhr.responseText,
+ code: (xhr.status >= 500) ? 4 : 2
+ };
+ return loader.trigger('error');
+ }
- // refresh live playlists after a target duration passes
- if (!loader.media().endList) {
- window.clearTimeout(mediaUpdateTimeout);
- mediaUpdateTimeout = window.setTimeout(function() {
- loader.trigger('mediaupdatetimeout');
- }, refreshDelay);
- }
+ loader.state = 'HAVE_METADATA';
- loader.trigger('loadedplaylist');
- };
+ parser = new videojs.m3u8.Parser();
+ parser.push(xhr.responseText);
+ parser.end();
+ parser.manifest.uri = url;
+
+ // merge this playlist into the master
+ update = updateMaster(loader.master, parser.manifest);
+ refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
+ if (update) {
+ loader.master = update;
+ loader.updateMediaPlaylist_(parser.manifest);
+ } else {
+ // if the playlist is unchanged since the last reload,
+ // try again after half the target duration
+ refreshDelay /= 2;
+ }
- PlaylistLoader.prototype.init.call(this);
+ // refresh live playlists after a target duration passes
+ if (!loader.media().endList) {
+ window.clearTimeout(mediaUpdateTimeout);
+ mediaUpdateTimeout = window.setTimeout(function() {
+ loader.trigger('mediaupdatetimeout');
+ }, refreshDelay);
+ }
- if (!srcUrl) {
- throw new Error('A non-empty playlist URL is required');
- }
+ loader.trigger('loadedplaylist');
+ };
+ // initialize the loader state
loader.state = 'HAVE_NOTHING';
+ // the total duration of all segments that expired and have been
+ // removed from the current playlist after the last
+ // #EXT-X-DISCONTINUITY. In a live playlist without
+ // discontinuities, this is the total amount of time that has
+ // been removed from the stream since the playlist loader began
+ // tracking it.
+ loader.expiredPostDiscontinuity_ = 0;
+
+ // the total duration of all segments that expired and have been
+ // removed from the current playlist before the last
+ // #EXT-X-DISCONTINUITY. The total amount of time that has
+ // expired is always the sum of expiredPreDiscontinuity_ and
+ // expiredPostDiscontinuity_.
+ loader.expiredPreDiscontinuity_ = 0;
+
// capture the prototype dispose function
dispose = this.dispose;
@@ -141,7 +170,7 @@
var mediaChange = false;
// getter
if (!playlist) {
- return media;
+ return loader.media_;
}
// setter
@@ -158,7 +187,7 @@
playlist = loader.master.playlists[playlist];
}
- mediaChange = playlist.uri !== media.uri;
+ mediaChange = playlist.uri !== loader.media_.uri;
// switch to fully loaded playlists immediately
if (loader.master.playlists[playlist.uri].endList) {
@@ -169,7 +198,7 @@
request = null;
}
loader.state = 'HAVE_METADATA';
- media = playlist;
+ loader.media_ = playlist;
// trigger media change if the active media has been updated
if (mediaChange) {
@@ -292,5 +321,46 @@
};
PlaylistLoader.prototype = new videojs.Hls.Stream();
+ /**
+ * Update the PlaylistLoader state to reflect the changes in an
+ * update to the current media playlist.
+ * @param update {object} the updated media playlist object
+ */
+ PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
+ var lastDiscontinuity, expiredCount, i;
+
+ if (this.media_) {
+ expiredCount = update.mediaSequence - this.media_.mediaSequence;
+
+ // setup the index for duration calculations so that the newly
+ // expired time will be accumulated after the last
+ // discontinuity, unless we discover otherwise
+ lastDiscontinuity = this.media_.mediaSequence;
+
+ if (this.media_.discontinuitySequence !== update.discontinuitySequence) {
+ i = expiredCount;
+ while (i--) {
+ if (this.media_.segments[i].discontinuity) {
+ // a segment that begins a new discontinuity sequence has expired
+ lastDiscontinuity = i + this.media_.mediaSequence;
+ this.expiredPreDiscontinuity_ += this.expiredPostDiscontinuity_;
+ this.expiredPostDiscontinuity_ = 0;
+ break;
+ }
+ }
+ }
+
+ // update the expirated durations
+ this.expiredPreDiscontinuity_ += Playlist.duration(this.media_,
+ this.media_.mediaSequence,
+ lastDiscontinuity);
+ this.expiredPostDiscontinuity_ += Playlist.duration(this.media_,
+ lastDiscontinuity,
+ this.media_.mediaSequence + expiredCount);
+ }
+
+ this.media_ = this.master.playlists[update.uri];
+ };
+
videojs.Hls.PlaylistLoader = PlaylistLoader;
})(window, window.videojs);
diff --git a/src/playlist.js b/src/playlist.js
new file mode 100644
index 000000000..7bce582d7
--- /dev/null
+++ b/src/playlist.js
@@ -0,0 +1,139 @@
+/**
+ * Playlist related utilities.
+ */
+(function(window, videojs) {
+ 'use strict';
+
+ var DEFAULT_TARGET_DURATION = 10;
+ var duration, seekable, segmentsDuration;
+
+ /**
+ * Calculate the media duration from the segments associated with a
+ * playlist. The duration of a subinterval of the available segments
+ * may be calculated by specifying a start and end index.
+ *
+ * @param playlist {object} a media playlist object
+ * @param startSequence {number} (optional) an inclusive lower
+ * boundary for the playlist. Defaults to 0.
+ * @param endSequence {number} (optional) an exclusive upper boundary
+ * for the playlist. Defaults to playlist length.
+ * @return {number} the duration between the start index and end
+ * index.
+ */
+ segmentsDuration = function(playlist, startSequence, endSequence) {
+ var targetDuration, i, segment, expiredSegmentCount, result = 0;
+
+ startSequence = startSequence || 0;
+ i = startSequence;
+ endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length;
+ targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
+
+ // estimate expired segment duration using the target duration
+ expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0);
+ result += expiredSegmentCount * targetDuration;
+ i += expiredSegmentCount;
+
+ // accumulate the segment durations into the result
+ for (; i < endSequence; i++) {
+ segment = playlist.segments[i - playlist.mediaSequence];
+ result += segment.preciseDuration ||
+ segment.duration ||
+ targetDuration;
+ }
+
+ return result;
+ };
+
+ /**
+ * Calculates the duration of a playlist. If a start and end index
+ * are specified, the duration will be for the subset of the media
+ * timeline between those two indices. The total duration for live
+ * playlists is always Infinity.
+ * @param playlist {object} a media playlist object
+ * @param startSequence {number} (optional) an inclusive lower
+ * boundary for the playlist. Defaults to 0.
+ * @param endSequence {number} (optional) an exclusive upper boundary
+ * for the playlist. Defaults to playlist length.
+ * @return {number} the duration between the start index and end
+ * index.
+ */
+ duration = function(playlist, startSequence, endSequence) {
+ if (!playlist) {
+ return 0;
+ }
+
+ // if a slice of the total duration is not requested, use
+ // playlist-level duration indicators when they're present
+ if (startSequence === undefined && endSequence === undefined) {
+ // if present, use the duration specified in the playlist
+ if (playlist.totalDuration) {
+ return playlist.totalDuration;
+ }
+
+ // duration should be Infinity for live playlists
+ if (!playlist.endList) {
+ return window.Infinity;
+ }
+ }
+
+ // calculate the total duration based on the segment durations
+ return segmentsDuration(playlist,
+ startSequence,
+ endSequence);
+ };
+
+ /**
+ * Calculates the interval of time that is currently seekable in a
+ * playlist.
+ * @param playlist {object} a media playlist object
+ * @return {TimeRanges} the periods of time that are valid targets
+ * for seeking
+ */
+ seekable = function(playlist) {
+ var start, end, liveBuffer, targetDuration, segment, pending, i;
+
+ // without segments, there are no seekable ranges
+ if (!playlist.segments) {
+ return videojs.createTimeRange();
+ }
+ // when the playlist is complete, the entire duration is seekable
+ if (playlist.endList) {
+ return videojs.createTimeRange(0, duration(playlist));
+ }
+
+ start = segmentsDuration(playlist, 0, playlist.mediaSequence);
+ end = start + segmentsDuration(playlist,
+ playlist.mediaSequence,
+ playlist.mediaSequence + playlist.segments.length);
+ targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
+
+ // live playlists should not expose three segment durations worth
+ // of content from the end of the playlist
+ // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
+ if (!playlist.endList) {
+ liveBuffer = targetDuration * 3;
+ // walk backward from the last available segment and track how
+ // much media time has elapsed until three target durations have
+ // been traversed. if a segment is part of the interval being
+ // reported, subtract the overlapping portion of its duration
+ // from the result.
+ for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) {
+ segment = playlist.segments[i];
+ pending = Math.min(segment.preciseDuration ||
+ segment.duration ||
+ targetDuration,
+ liveBuffer);
+ liveBuffer -= pending;
+ end -= pending;
+ }
+ }
+
+ return videojs.createTimeRange(start, end);
+ };
+
+ // exports
+ videojs.Hls.Playlist = {
+ duration: duration,
+ seekable: seekable
+ };
+})(window, window.videojs);
diff --git a/src/segment-parser.js b/src/segment-parser.js
index f07362454..00189f262 100644
--- a/src/segment-parser.js
+++ b/src/segment-parser.js
@@ -19,10 +19,7 @@
streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH),
streamBufferByteCount = 0,
h264Stream = new H264Stream(),
- aacStream = new AacStream(),
- h264HasTimeStampOffset = false,
- aacHasTimeStampOffset = false,
- timeStampOffset;
+ aacStream = new AacStream();
// expose the stream metadata
self.stream = {
@@ -34,6 +31,15 @@
// allow in-band metadata to be observed
self.metadataStream = new MetadataStream();
+ this.mediaTimelineOffset = null;
+
+ // The first timestamp value encountered during parsing. This
+ // value can be used to determine the relative timing between
+ // frames and the start of the current timestamp sequence. It
+ // should be reset to null before parsing a segment with
+ // discontinuous timestamp values from previous segments.
+ self.timestampOffset = null;
+
// For information on the FLV format, see
// http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
// Technically, this function returns the header and a metadata FLV tag
@@ -354,31 +360,18 @@
// Skip past "optional" portion of PTS header
offset += pesHeaderLength;
- // align the metadata stream PTS values with the start of
- // the other elementary streams
- if (!self.metadataStream.timestampOffset) {
- self.metadataStream.timestampOffset = pts;
+ // keep track of the earliest encounted PTS value so
+ // external parties can align timestamps across
+ // discontinuities
+ if (self.timestampOffset === null) {
+ self.timestampOffset = pts;
}
if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
- if (!h264HasTimeStampOffset) {
- h264HasTimeStampOffset = true;
- if (timeStampOffset === undefined) {
- timeStampOffset = pts;
- }
- h264Stream.setTimeStampOffset(timeStampOffset);
- }
h264Stream.setNextTimeStamp(pts,
dts,
dataAlignmentIndicator);
} else if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
- if (!aacHasTimeStampOffset) {
- aacHasTimeStampOffset = true;
- if (timeStampOffset === undefined) {
- timeStampOffset = pts;
- }
- aacStream.setTimeStampOffset(timeStampOffset);
- }
aacStream.setNextTimeStamp(pts,
pesPacketSize,
dataAlignmentIndicator);
diff --git a/src/videojs-hls.js b/src/videojs-hls.js
index e7165369b..b9ef42883 100644
--- a/src/videojs-hls.js
+++ b/src/videojs-hls.js
@@ -39,9 +39,11 @@ videojs.Hls = videojs.Flash.extend({
this.currentTime = videojs.Hls.prototype.currentTime;
this.setCurrentTime = videojs.Hls.prototype.setCurrentTime;
+ // a queue of segments that need to be transmuxed and processed,
+ // and then fed to the source buffer
+ this.segmentBuffer_ = [];
// periodically check if new data needs to be downloaded or
// buffered data should be appended to the source buffer
- this.segmentBuffer_ = [];
this.startCheckingBuffer_();
videojs.Hls.prototype.src.call(this, options.source && options.source.src);
@@ -87,43 +89,7 @@ videojs.Hls.prototype.src = function(src) {
// if the stream contains ID3 metadata, expose that as a metadata
// text track
- (function() {
- var
- metadataStream = tech.segmentParser_.metadataStream,
- textTrack;
-
- // only expose metadata tracks to video.js versions that support
- // dynamic text tracks (4.12+)
- if (!tech.player().addTextTrack) {
- return;
- }
-
- metadataStream.on('data', function(metadata) {
- var i, cue, frame, time, hexDigit;
-
- // create the metadata track if this is the first ID3 tag we've
- // seen
- if (!textTrack) {
- textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata');
-
- // build the dispatch type from the stream descriptor
- // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
- textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase();
- for (i = 0; i < metadataStream.descriptor.length; i++) {
- hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2);
- textTrack.inBandMetadataTrackDispatchType += hexDigit;
- }
- }
-
- for (i = 0; i < metadata.frames.length; i++) {
- frame = metadata.frames[i];
- time = metadata.pts / 1000;
- cue = new window.VTTCue(time, time, frame.value || frame.url || '');
- cue.frame = frame;
- textTrack.addCue(cue);
- }
- });
- })();
+ this.setupMetadataCueTranslation_();
// load the MediaSource into the player
this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen));
@@ -280,6 +246,75 @@ videojs.Hls.prototype.handleSourceOpen = function() {
}
};
+// register event listeners to transform in-band metadata events into
+// VTTCues on a text track
+videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
+ var
+ tech = this,
+ metadataStream = tech.segmentParser_.metadataStream,
+ textTrack;
+
+ // only expose metadata tracks to video.js versions that support
+ // dynamic text tracks (4.12+)
+ if (!tech.player().addTextTrack) {
+ return;
+ }
+
+ // add a metadata cue whenever a metadata event is triggered during
+ // segment parsing
+ metadataStream.on('data', function(metadata) {
+ var i, cue, frame, time, media, segmentOffset, hexDigit;
+
+ // create the metadata track if this is the first ID3 tag we've
+ // seen
+ if (!textTrack) {
+ textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata');
+
+ // build the dispatch type from the stream descriptor
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
+ textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase();
+ for (i = 0; i < metadataStream.descriptor.length; i++) {
+ hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2);
+ textTrack.inBandMetadataTrackDispatchType += hexDigit;
+ }
+ }
+
+ // calculate the start time for the segment that is currently being parsed
+ media = tech.playlists.media();
+ segmentOffset = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_;
+ segmentOffset += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex);
+
+ // create cue points for all the ID3 frames in this metadata event
+ for (i = 0; i < metadata.frames.length; i++) {
+ frame = metadata.frames[i];
+ time = tech.segmentParser_.mediaTimelineOffset + ((metadata.pts - tech.segmentParser_.timestampOffset) * 0.001);
+ cue = new window.VTTCue(time, time, frame.value || frame.url || '');
+ cue.frame = frame;
+ textTrack.addCue(cue);
+ }
+ });
+
+ // when seeking, clear out all cues ahead of the earliest position
+ // in the new segment. keep earlier cues around so they can still be
+ // programmatically inspected even though they've already fired
+ tech.on(tech.player(), 'seeking', function() {
+ var media, startTime, i;
+ if (!textTrack) {
+ return;
+ }
+ media = tech.playlists.media();
+ startTime = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_;
+ startTime += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex);
+
+ i = textTrack.cues.length;
+ while (i--) {
+ if (textTrack.cues[i].startTime >= startTime) {
+ textTrack.removeCue(textTrack.cues[i]);
+ }
+ }
+ });
+};
+
/**
* Reset the mediaIndex if play() is called after the video has
* ended.
@@ -350,18 +385,37 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
videojs.Hls.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
- return videojs.Hls.getPlaylistTotalDuration(playlists.media());
+ return videojs.Hls.Playlist.duration(playlists.media());
}
return 0;
};
+videojs.Hls.prototype.seekable = function() {
+ var absoluteSeekable, startOffset, media;
+
+ if (!this.playlists) {
+ return videojs.createTimeRange();
+ }
+ media = this.playlists.media();
+ if (!media) {
+ return videojs.createTimeRange();
+ }
+
+ // report the seekable range relative to the earliest possible
+ // position when the stream was first loaded
+ absoluteSeekable = videojs.Hls.Playlist.seekable(media);
+ startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_;
+ return videojs.createTimeRange(startOffset,
+ startOffset + (absoluteSeekable.end(0) - absoluteSeekable.start(0)));
+};
+
/**
* Update the player duration
*/
videojs.Hls.prototype.updateDuration = function(playlist) {
var player = this.player(),
oldDuration = player.duration(),
- newDuration = videojs.Hls.getPlaylistTotalDuration(playlist);
+ newDuration = videojs.Hls.Playlist.duration(playlist);
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
@@ -684,9 +738,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
tech.setBandwidth(this);
// package up all the work to append the segment
- // if the segment is the start of a timestamp discontinuity,
- // we have to wait until the sourcebuffer is empty before
- // aborting the source buffer processing
segmentInfo = {
// the segment's mediaIndex at the time it was received
mediaIndex: tech.mediaIndex,
@@ -789,7 +840,20 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
event = event || {};
- segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
+ segmentOffset = this.playlists.expiredPreDiscontinuity_;
+ segmentOffset += this.playlists.expiredPostDiscontinuity_;
+ segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex);
+ segmentOffset *= 1000;
+
+ // if this segment starts is the start of a new discontinuity
+ // sequence, the segment parser's timestamp offset must be
+ // re-calculated
+ if (segment.discontinuity) {
+ this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001;
+ this.segmentParser_.timestampOffset = null;
+ } else if (this.segmentParser_.mediaTimelineOffset === null) {
+ this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001;
+ }
// transmux the segment data from MP2T to FLV
this.segmentParser_.parseSegmentBinaryData(bytes);
@@ -801,10 +865,10 @@ videojs.Hls.prototype.drainBuffer = function(event) {
tags.push(this.segmentParser_.getNextTag());
}
- // Use the presentation timestamp of the ts segment to calculate its
- // exact duration, since this may differ by fractions of a second
- // from what is reported in the playlist
if (tags.length > 0) {
+ // Use the presentation timestamp of the ts segment to calculate its
+ // exact duration, since this may differ by fractions of a second
+ // from what is reported in the playlist
segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001;
}
@@ -976,20 +1040,9 @@ videojs.Hls.canPlaySource = function(srcObj) {
* @return {number} the duration between the start index and end index.
*/
videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
- var dur = 0,
- segment,
- i;
-
- startIndex = startIndex || 0;
- endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
- i = endIndex - 1;
-
- for (; i >= startIndex; i--) {
- segment = playlist.segments[i];
- dur += segment.preciseDuration || segment.duration || playlist.targetDuration || 0;
- }
-
- return dur;
+ videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' +
+ 'Use videojs.Hls.Playlist.duration instead');
+ return videojs.Hls.Playlist.duration(playlist, startIndex, endIndex);
};
/**
@@ -998,21 +1051,9 @@ videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
* @return {number} the currently known duration, in seconds
*/
videojs.Hls.getPlaylistTotalDuration = function(playlist) {
- if (!playlist) {
- return 0;
- }
-
- // if present, use the duration specified in the playlist
- if (playlist.totalDuration) {
- return playlist.totalDuration;
- }
-
- // duration should be Infinity for live playlists
- if (!playlist.endList) {
- return window.Infinity;
- }
-
- return videojs.Hls.getPlaylistDuration(playlist);
+ videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' +
+ 'Use videojs.Hls.Playlist.duration instead');
+ return videojs.Hls.Playlist.duration(playlist);
};
/**
diff --git a/test/h264-stream_test.js b/test/h264-stream_test.js
index d36fb4489..7ea263f09 100644
--- a/test/h264-stream_test.js
+++ b/test/h264-stream_test.js
@@ -61,45 +61,6 @@ test('metadata is generated for IDRs after a full NAL unit is written', function
ok(h264Stream.tags[2].keyFrame, 'key frame is written');
});
-test('starting PTS values can be negative', function() {
- var
- H264ExtraData = videojs.Hls.H264ExtraData,
- oldExtraData = H264ExtraData.prototype.extraDataTag,
- oldMetadata = H264ExtraData.prototype.metaDataTag,
- h264Stream;
-
- H264ExtraData.prototype.extraDataTag = function() {
- return 'extraDataTag';
- };
- H264ExtraData.prototype.metaDataTag = function() {
- return 'metaDataTag';
- };
-
- h264Stream = new videojs.Hls.H264Stream();
-
- h264Stream.setTimeStampOffset(-100);
- h264Stream.setNextTimeStamp(-100, -100, true);
- h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
- h264Stream.setNextTimeStamp(-99, -99, true);
- h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
- h264Stream.setNextTimeStamp(0, 0, true);
- h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
- // flush out the last tag
- h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
-
- strictEqual(h264Stream.tags.length, 3, 'three tags are ready');
- strictEqual(h264Stream.tags[0].pts, 0, 'the first PTS is zero');
- strictEqual(h264Stream.tags[0].dts, 0, 'the first DTS is zero');
- strictEqual(h264Stream.tags[1].pts, 1, 'the second PTS is one');
- strictEqual(h264Stream.tags[1].dts, 1, 'the second DTS is one');
-
- strictEqual(h264Stream.tags[2].pts, 100, 'the third PTS is 100');
- strictEqual(h264Stream.tags[2].dts, 100, 'the third DTS is 100');
-
- H264ExtraData.prototype.extraDataTag = oldExtraData;
- H264ExtraData.prototype.metaDataTag = oldMetadata;
-});
-
test('make sure we add metadata and extra data at the beginning of a stream', function() {
var
H264ExtraData = videojs.Hls.H264ExtraData,
diff --git a/test/karma.conf.js b/test/karma.conf.js
index 6fb6d66c4..841204a8f 100644
--- a/test/karma.conf.js
+++ b/test/karma.conf.js
@@ -75,7 +75,7 @@ module.exports = function(config) {
'../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js',
'../node_modules/sinon/lib/sinon/util/xhr_ie.js',
'../node_modules/sinon/lib/sinon/util/fake_timers.js',
- '../node_modules/video.js/dist/video-js/video.js',
+ '../node_modules/video.js/dist/video-js/video.dev.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',
@@ -90,6 +90,7 @@ module.exports = function(config) {
'../src/segment-parser.js',
'../src/m3u8/m3u8-parser.js',
'../src/xhr.js',
+ '../src/playlist.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
diff --git a/test/localkarma.conf.js b/test/localkarma.conf.js
index 304610421..2d08cf5d4 100644
--- a/test/localkarma.conf.js
+++ b/test/localkarma.conf.js
@@ -39,7 +39,7 @@ module.exports = function(config) {
'../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js',
'../node_modules/sinon/lib/sinon/util/xhr_ie.js',
'../node_modules/sinon/lib/sinon/util/fake_timers.js',
- '../node_modules/video.js/dist/video-js/video.js',
+ '../node_modules/video.js/dist/video-js/video.dev.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',
@@ -54,6 +54,7 @@ module.exports = function(config) {
'../src/segment-parser.js',
'../src/m3u8/m3u8-parser.js',
'../src/xhr.js',
+ '../src/playlist.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
diff --git a/test/manifest/absoluteUris.js b/test/manifest/absoluteUris.js
index 68dd2ea56..208de13f4 100644
--- a/test/manifest/absoluteUris.js
+++ b/test/manifest/absoluteUris.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/allowCache.js b/test/manifest/allowCache.js
index 629c49a38..58e49317a 100644
--- a/test/manifest/allowCache.js
+++ b/test/manifest/allowCache.js
@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/allowCacheInvalid.js b/test/manifest/allowCacheInvalid.js
index 12136cd64..b721a9c8c 100644
--- a/test/manifest/allowCacheInvalid.js
+++ b/test/manifest/allowCacheInvalid.js
@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/byteRange.js b/test/manifest/byteRange.js
index 6b0f450af..324e829e0 100644
--- a/test/manifest/byteRange.js
+++ b/test/manifest/byteRange.js
@@ -137,5 +137,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/disallowCache.js b/test/manifest/disallowCache.js
index 0d6617f6c..a297080ff 100644
--- a/test/manifest/disallowCache.js
+++ b/test/manifest/disallowCache.js
@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/disc-sequence.js b/test/manifest/disc-sequence.js
new file mode 100644
index 000000000..4c84a97bb
--- /dev/null
+++ b/test/manifest/disc-sequence.js
@@ -0,0 +1,26 @@
+{
+ "allowCache": true,
+ "mediaSequence": 0,
+ "discontinuitySequence": 3,
+ "segments": [
+ {
+ "duration": 10,
+ "uri": "001.ts"
+ },
+ {
+ "duration": 19,
+ "uri": "002.ts"
+ },
+ {
+ "discontinuity": true,
+ "duration": 10,
+ "uri": "003.ts"
+ },
+ {
+ "duration": 11,
+ "uri": "004.ts"
+ }
+ ],
+ "targetDuration": 19,
+ "endList": true
+}
diff --git a/test/manifest/disc-sequence.m3u8 b/test/manifest/disc-sequence.m3u8
new file mode 100644
index 000000000..5ac89ba48
--- /dev/null
+++ b/test/manifest/disc-sequence.m3u8
@@ -0,0 +1,15 @@
+#EXTM3U
+#EXT-X-VERSION:3
+#EXT-X-TARGETDURATION:19
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-DISCONTINUITY-SEQUENCE:3
+#EXTINF:10,0
+001.ts
+#EXTINF:19,0
+002.ts
+#EXT-X-DISCONTINUITY
+#EXTINF:10,0
+003.ts
+#EXTINF:11,0
+004.ts
+#EXT-X-ENDLIST
diff --git a/test/manifest/discontinuity.js b/test/manifest/discontinuity.js
index bf0e07627..8b9d874df 100644
--- a/test/manifest/discontinuity.js
+++ b/test/manifest/discontinuity.js
@@ -1,6 +1,7 @@
{
"allowCache": true,
"mediaSequence": 0,
+ "discontinuitySequence": 0,
"segments": [
{
"duration": 10,
diff --git a/test/manifest/domainUris.js b/test/manifest/domainUris.js
index 25de9ae5f..fc9504696 100644
--- a/test/manifest/domainUris.js
+++ b/test/manifest/domainUris.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/emptyAllowCache.js b/test/manifest/emptyAllowCache.js
index 12136cd64..b721a9c8c 100644
--- a/test/manifest/emptyAllowCache.js
+++ b/test/manifest/emptyAllowCache.js
@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/emptyMediaSequence.js b/test/manifest/emptyMediaSequence.js
index be7fa289a..b5f53cd01 100644
--- a/test/manifest/emptyMediaSequence.js
+++ b/test/manifest/emptyMediaSequence.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/emptyPlaylistType.js b/test/manifest/emptyPlaylistType.js
index 1590d6827..fc4c2aec5 100644
--- a/test/manifest/emptyPlaylistType.js
+++ b/test/manifest/emptyPlaylistType.js
@@ -28,5 +28,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/encrypted.js b/test/manifest/encrypted.js
index d6f296e0d..80ad8eaa8 100644
--- a/test/manifest/encrypted.js
+++ b/test/manifest/encrypted.js
@@ -1,6 +1,7 @@
{
"allowCache": true,
"mediaSequence": 7794,
+ "discontinuitySequence": 0,
"segments": [
{
"duration": 2.833,
diff --git a/test/manifest/event.js b/test/manifest/event.js
index 8727ff064..9ec9248b6 100644
--- a/test/manifest/event.js
+++ b/test/manifest/event.js
@@ -29,5 +29,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/extXPlaylistTypeInvalidPlaylist.js b/test/manifest/extXPlaylistTypeInvalidPlaylist.js
index 94030aad8..760713d15 100644
--- a/test/manifest/extXPlaylistTypeInvalidPlaylist.js
+++ b/test/manifest/extXPlaylistTypeInvalidPlaylist.js
@@ -8,5 +8,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/extinf.js b/test/manifest/extinf.js
index cdbd5346e..be5f64887 100644
--- a/test/manifest/extinf.js
+++ b/test/manifest/extinf.js
@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/invalidAllowCache.js b/test/manifest/invalidAllowCache.js
index 12136cd64..b721a9c8c 100644
--- a/test/manifest/invalidAllowCache.js
+++ b/test/manifest/invalidAllowCache.js
@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/invalidMediaSequence.js b/test/manifest/invalidMediaSequence.js
index be7fa289a..b5f53cd01 100644
--- a/test/manifest/invalidMediaSequence.js
+++ b/test/manifest/invalidMediaSequence.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/invalidPlaylistType.js b/test/manifest/invalidPlaylistType.js
index 1590d6827..fc4c2aec5 100644
--- a/test/manifest/invalidPlaylistType.js
+++ b/test/manifest/invalidPlaylistType.js
@@ -28,5 +28,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/invalidTargetDuration.js b/test/manifest/invalidTargetDuration.js
index fde781293..3686308d6 100644
--- a/test/manifest/invalidTargetDuration.js
+++ b/test/manifest/invalidTargetDuration.js
@@ -140,5 +140,6 @@
"uri": "hls_450k_video.ts"
}
],
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/liveMissingSegmentDuration.js b/test/manifest/liveMissingSegmentDuration.js
index a2c18395c..95bc58972 100644
--- a/test/manifest/liveMissingSegmentDuration.js
+++ b/test/manifest/liveMissingSegmentDuration.js
@@ -16,5 +16,6 @@
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
}
],
- "targetDuration": 8
+ "targetDuration": 8,
+ "discontinuitySequence": 0
}
diff --git a/test/manifest/liveStart30sBefore.js b/test/manifest/liveStart30sBefore.js
index eae226c63..068f7d06c 100644
--- a/test/manifest/liveStart30sBefore.js
+++ b/test/manifest/liveStart30sBefore.js
@@ -39,5 +39,6 @@
"uri": "009.ts"
}
],
- "targetDuration": 10
-}
\ No newline at end of file
+ "targetDuration": 10,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/manifestExtTTargetdurationNegative.js b/test/manifest/manifestExtTTargetdurationNegative.js
index 7c75f85d5..f34e556e1 100644
--- a/test/manifest/manifestExtTTargetdurationNegative.js
+++ b/test/manifest/manifestExtTTargetdurationNegative.js
@@ -7,5 +7,6 @@
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
],
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/manifestExtXEndlistEarly.js b/test/manifest/manifestExtXEndlistEarly.js
index 99fb4de46..ff8dc2339 100644
--- a/test/manifest/manifestExtXEndlistEarly.js
+++ b/test/manifest/manifestExtXEndlistEarly.js
@@ -24,5 +24,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/manifestNoExtM3u.js b/test/manifest/manifestNoExtM3u.js
index 578f265e4..25c129e3a 100644
--- a/test/manifest/manifestNoExtM3u.js
+++ b/test/manifest/manifestNoExtM3u.js
@@ -8,5 +8,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/media.js b/test/manifest/media.js
index 4a07d9b50..9b68ea124 100644
--- a/test/manifest/media.js
+++ b/test/manifest/media.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
- "endList": true
+ "endList": true,
+ "discontinuitySequence": 0
}
diff --git a/test/manifest/mediaSequence.js b/test/manifest/mediaSequence.js
index be7fa289a..b5f53cd01 100644
--- a/test/manifest/mediaSequence.js
+++ b/test/manifest/mediaSequence.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/missingEndlist.js b/test/manifest/missingEndlist.js
index 49e1db8bc..6d9894876 100644
--- a/test/manifest/missingEndlist.js
+++ b/test/manifest/missingEndlist.js
@@ -11,5 +11,6 @@
"uri": "00002.ts"
}
],
- "targetDuration": 10
+ "targetDuration": 10,
+ "discontinuitySequence": 0
}
diff --git a/test/manifest/missingExtinf.js b/test/manifest/missingExtinf.js
index d20d6c6f0..a82e9a58c 100644
--- a/test/manifest/missingExtinf.js
+++ b/test/manifest/missingExtinf.js
@@ -17,5 +17,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/missingMediaSequence.js b/test/manifest/missingMediaSequence.js
index be7fa289a..b5f53cd01 100644
--- a/test/manifest/missingMediaSequence.js
+++ b/test/manifest/missingMediaSequence.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/missingSegmentDuration.js b/test/manifest/missingSegmentDuration.js
index beaab50be..b34d052f1 100644
--- a/test/manifest/missingSegmentDuration.js
+++ b/test/manifest/missingSegmentDuration.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/multipleTargetDurations.js b/test/manifest/multipleTargetDurations.js
index e67342672..ca77d2817 100644
--- a/test/manifest/multipleTargetDurations.js
+++ b/test/manifest/multipleTargetDurations.js
@@ -18,5 +18,6 @@
"uri": "004.ts",
"duration": 10
}
- ]
+ ],
+ "discontinuitySequence": 0
}
diff --git a/test/manifest/negativeMediaSequence.js b/test/manifest/negativeMediaSequence.js
index 549350f86..0e1cd7c41 100644
--- a/test/manifest/negativeMediaSequence.js
+++ b/test/manifest/negativeMediaSequence.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/playlist.js b/test/manifest/playlist.js
index 629c49a38..58e49317a 100644
--- a/test/manifest/playlist.js
+++ b/test/manifest/playlist.js
@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/playlistMediaSequenceHigher.js b/test/manifest/playlistMediaSequenceHigher.js
index 386ddf132..a1526c630 100644
--- a/test/manifest/playlistMediaSequenceHigher.js
+++ b/test/manifest/playlistMediaSequenceHigher.js
@@ -9,5 +9,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/twoMediaSequences.js b/test/manifest/twoMediaSequences.js
index 85ada1581..af7dd8a6d 100644
--- a/test/manifest/twoMediaSequences.js
+++ b/test/manifest/twoMediaSequences.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/versionInvalid.js b/test/manifest/versionInvalid.js
index 759b1ac42..a100a159b 100644
--- a/test/manifest/versionInvalid.js
+++ b/test/manifest/versionInvalid.js
@@ -9,5 +9,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/manifest/whiteSpace.js b/test/manifest/whiteSpace.js
index 68dd2ea56..208de13f4 100644
--- a/test/manifest/whiteSpace.js
+++ b/test/manifest/whiteSpace.js
@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
- "endList": true
-}
\ No newline at end of file
+ "endList": true,
+ "discontinuitySequence": 0
+}
diff --git a/test/metadata-stream_test.js b/test/metadata-stream_test.js
index bf6633b3e..e9c4557aa 100644
--- a/test/metadata-stream_test.js
+++ b/test/metadata-stream_test.js
@@ -186,28 +186,6 @@
// too large/small tag size values
// too large/small frame size values
- test('translates PTS and DTS values based on the timestamp offset', function() {
- var events = [];
- metadataStream.on('data', function(event) {
- events.push(event);
- });
-
- metadataStream.timestampOffset = 800;
-
- metadataStream.push({
- trackId: 7,
- pts: 1000,
- dts: 900,
-
- // header
- data: new Uint8Array(id3Tag(id3Frame('XFFF', [0]), [0x00, 0x00]))
- });
-
- equal(events.length, 1, 'emitted an event');
- equal(events[0].pts, 200, 'translated pts');
- equal(events[0].dts, 100, 'translated dts');
- });
-
test('parses TXXX frames', function() {
var events = [];
metadataStream.on('data', function(event) {
@@ -223,7 +201,7 @@
data: new Uint8Array(id3Tag(id3Frame('TXXX',
0x03, // utf-8
stringToCString('get done'),
- stringToInts('{ "key": "value" }')),
+ stringToCString('{ "key": "value" }')),
[0x00, 0x00]))
});
@@ -231,7 +209,7 @@
equal(events[0].frames.length, 1, 'parsed one frame');
equal(events[0].frames[0].id, 'TXXX', 'parsed the frame id');
equal(events[0].frames[0].description, 'get done', 'parsed the description');
- equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value');
+ deepEqual(JSON.parse(events[0].frames[0].value), { key: 'value' }, 'parsed the value');
});
test('parses WXXX frames', function() {
@@ -275,7 +253,7 @@
data: new Uint8Array(id3Tag(id3Frame('TXXX',
0x03, // utf-8
stringToCString(''),
- stringToInts(value)),
+ stringToCString(value)),
[0x00, 0x00]))
});
diff --git a/test/playlist-loader_test.js b/test/playlist-loader_test.js
index 917348b1e..619683d0a 100644
--- a/test/playlist-loader_test.js
+++ b/test/playlist-loader_test.js
@@ -50,6 +50,20 @@
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.expiredPreDiscontinuity_,
+ 0,
+ 'zero seconds expired pre-discontinuity');
+ equal(loader.expiredPostDiscontinuity_,
+ 0,
+ 'zero seconds expired post-discontinuity');
+ });
+
test('requests the initial playlist immediately', function() {
new videojs.Hls.PlaylistLoader('master.m3u8');
strictEqual(requests.length, 1, 'made a request');
@@ -160,6 +174,105 @@
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.expiredPostDiscontinuity_, 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.expiredPreDiscontinuity_, 0, 'identifies pre-discontinuity time');
+ equal(loader.expiredPostDiscontinuity_, 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.expiredPreDiscontinuity_, 0, 'tracked time across the discontinuity');
+ equal(loader.expiredPostDiscontinuity_, 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.expiredPreDiscontinuity_, 13, 'did not increment pre-discontinuity');
+ equal(loader.expiredPostDiscontinuity_, 4, 'expired post-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.expiredPreDiscontinuity_, 4 + 5, 'tracked pre-discontinuity time');
+ equal(loader.expiredPostDiscontinuity_, 6, 'tracked post-discontinuity time');
+ });
+
test('emits an error when an initial playlist request fails', function() {
var
errors = [],
@@ -597,4 +710,5 @@
'#EXT-X-ENDLIST'); // no newline
ok(loader.media().endList, 'flushed the final line of input');
});
+
})(window);
diff --git a/test/playlist_test.js b/test/playlist_test.js
new file mode 100644
index 000000000..4a249ed8c
--- /dev/null
+++ b/test/playlist_test.js
@@ -0,0 +1,138 @@
+/* Tests for the playlist utilities */
+(function(window, videojs) {
+ 'use strict';
+ var Playlist = videojs.Hls.Playlist;
+
+ module('Playlist Utilities');
+
+ test('total duration for live playlists is Infinity', function() {
+ var duration = Playlist.duration({
+ segments: [{
+ duration: 4,
+ uri: '0.ts'
+ }]
+ });
+
+ equal(duration, Infinity, 'duration is infinity');
+ });
+
+ test('interval duration accounts for media sequences', function() {
+ var duration = Playlist.duration({
+ mediaSequence: 10,
+ endList: true,
+ segments: [{
+ duration: 10,
+ uri: '10.ts'
+ }, {
+ duration: 10,
+ uri: '11.ts'
+ }, {
+ duration: 10,
+ uri: '12.ts'
+ }, {
+ duration: 10,
+ uri: '13.ts'
+ }]
+ }, 0, 14);
+
+ equal(duration, 14 * 10, 'duration includes dropped segments');
+ });
+
+ test('calculates seekable time ranges from the available segments', function() {
+ var playlist = {
+ mediaSequence: 0,
+ segments: [{
+ duration: 10,
+ uri: '0.ts'
+ }, {
+ duration: 10,
+ uri: '1.ts'
+ }],
+ endList: true
+ }, seekable = Playlist.seekable(playlist);
+
+ equal(seekable.length, 1, 'there are seekable ranges');
+ equal(seekable.start(0), 0, 'starts at zero');
+ equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
+ });
+
+ test('master playlists have empty seekable ranges', function() {
+ var seekable = Playlist.seekable({
+ playlists: [{
+ uri: 'low.m3u8'
+ }, {
+ uri: 'high.m3u8'
+ }]
+ });
+ equal(seekable.length, 0, 'no seekable ranges from a master playlist');
+ });
+
+ test('seekable end is three target durations from the actual end of live playlists', function() {
+ var seekable = Playlist.seekable({
+ mediaSequence: 0,
+ segments: [{
+ duration: 7,
+ uri: '0.ts'
+ }, {
+ duration: 10,
+ uri: '1.ts'
+ }, {
+ duration: 10,
+ uri: '2.ts'
+ }, {
+ duration: 10,
+ uri: '3.ts'
+ }]
+ });
+ equal(seekable.length, 1, 'there are seekable ranges');
+ equal(seekable.start(0), 0, 'starts at zero');
+ equal(seekable.end(0), 7, 'ends three target durations from the last segment');
+ });
+
+ test('adjusts seekable to the live playlist window', function() {
+ var seekable = Playlist.seekable({
+ targetDuration: 10,
+ mediaSequence: 7,
+ segments: [{
+ uri: '8.ts'
+ }, {
+ uri: '9.ts'
+ }, {
+ uri: '10.ts'
+ }, {
+ uri: '11.ts'
+ }]
+ });
+ equal(seekable.length, 1, 'there are seekable ranges');
+ equal(seekable.start(0), 10 * 7, 'starts at the earliest available segment');
+ equal(seekable.end(0), 10 * 8, 'ends three target durations from the last available segment');
+ });
+
+ test('seekable end accounts for non-standard target durations', function() {
+ var seekable = Playlist.seekable({
+ targetDuration: 2,
+ mediaSequence: 0,
+ segments: [{
+ duration: 2,
+ uri: '0.ts'
+ }, {
+ duration: 2,
+ uri: '1.ts'
+ }, {
+ duration: 1,
+ uri: '2.ts'
+ }, {
+ duration: 2,
+ uri: '3.ts'
+ }, {
+ duration: 2,
+ uri: '4.ts'
+ }]
+ });
+ equal(seekable.start(0), 0, 'starts at the earliest available segment');
+ equal(seekable.end(0),
+ 9 - (2 * 3),
+ 'allows seeking no further than three target durations from the end');
+ });
+
+})(window, window.videojs);
diff --git a/test/videojs-hls.html b/test/videojs-hls.html
index 8c03119fa..6488c8fe9 100644
--- a/test/videojs-hls.html
+++ b/test/videojs-hls.html
@@ -15,7 +15,7 @@
-
+
@@ -32,6 +32,7 @@
+
@@ -59,6 +60,7 @@
+
diff --git a/test/videojs-hls_test.js b/test/videojs-hls_test.js
index bef9a017f..d15ba7ff1 100644
--- a/test/videojs-hls_test.js
+++ b/test/videojs-hls_test.js
@@ -293,6 +293,31 @@ test('calculates the duration if needed', function() {
'duration is calculated');
});
+test('translates seekable by the starting time for live playlists', function() {
+ var seekable;
+ player.src({
+ src: 'media.m3u8',
+ type: 'application/vnd.apple.mpegurl'
+ });
+ openMediaSource(player);
+ requests.shift().respond(200, null,
+ '#EXTM3U\n' +
+ '#EXT-X-MEDIA-SEQUENCE:15\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');
+
+ seekable = player.seekable();
+ equal(seekable.length, 1, 'one seekable range');
+ equal(seekable.start(0), 0, 'the earliest possible position is at zero');
+ equal(seekable.end(0), 10, 'end is relative to the start');
+});
+
test('starts downloading a segment on loadedmetadata', function() {
player.src({
src: 'manifest/media.m3u8',
@@ -1224,6 +1249,226 @@ test('exposes in-band metadata events as cues', function() {
'set the private data');
});
+test('only adds in-band cues the first time they are encountered', function() {
+ var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track;
+ player.src({
+ src: 'manifest/media.m3u8',
+ type: 'application/vnd.apple.mpegurl'
+ });
+ openMediaSource(player);
+
+ player.hls.segmentParser_.getNextTag = function() {
+ return tags.shift();
+ };
+ player.hls.segmentParser_.tagsAvailable = function() {
+ return tags.length;
+ };
+ player.hls.segmentParser_.parseSegmentBinaryData = function() {
+ // fake out a descriptor
+ player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
+ 1, 2, 3, 0xbb
+ ]);
+ // trigger a metadata event
+ player.hls.segmentParser_.metadataStream.trigger('data', {
+ pts: 2000,
+ data: new Uint8Array([]),
+ frames: [{
+ id: 'TXXX',
+ value: 'cue text'
+ }]
+ });
+ };
+ standardXHRResponse(requests.shift());
+ standardXHRResponse(requests.shift());
+ // seek back to the first segment
+ player.currentTime(0);
+ player.hls.trigger('seeking');
+ tags.push({ pts: 0, bytes: new Uint8Array(1) });
+ standardXHRResponse(requests.shift());
+
+ track = player.textTracks()[0];
+ equal(track.cues.length, 1, 'only added the cue once');
+});
+
+test('clears in-band cues ahead of current time on seek', function() {
+ var
+ tags = [],
+ events = [],
+ track;
+ player.src({
+ src: 'manifest/media.m3u8',
+ type: 'application/vnd.apple.mpegurl'
+ });
+ openMediaSource(player);
+
+ player.hls.segmentParser_.getNextTag = function() {
+ return tags.shift();
+ };
+ player.hls.segmentParser_.tagsAvailable = function() {
+ return tags.length;
+ };
+ player.hls.segmentParser_.parseSegmentBinaryData = function() {
+ // fake out a descriptor
+ player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
+ 1, 2, 3, 0xbb
+ ]);
+ // trigger a metadata event
+ if (events.length) {
+ player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
+ }
+ };
+ standardXHRResponse(requests.shift()); // media
+ tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) });
+ events.push({
+ pts: 20 * 1000,
+ data: new Uint8Array([]),
+ frames: [{
+ id: 'TXXX',
+ value: 'cue 3'
+ }]
+ });
+ events.push({
+ pts: 9.9 * 1000,
+ data: new Uint8Array([]),
+ frames: [{
+ id: 'TXXX',
+ value: 'cue 1'
+ }]
+ });
+ standardXHRResponse(requests.shift()); // segment 0
+ tags.push({ pts: 20 * 1000, bytes: new Uint8Array(1) });
+ events.push({
+ pts: 19.9 * 1000,
+ data: new Uint8Array([]),
+ frames: [{
+ id: 'TXXX',
+ value: 'cue 2'
+ }]
+ });
+ player.hls.checkBuffer_();
+ standardXHRResponse(requests.shift()); // segment 1
+
+ track = player.textTracks()[0];
+ equal(track.cues.length, 2, 'added the cues');
+
+ // seek into segment 1
+ player.currentTime(11);
+ player.trigger('seeking');
+ equal(track.cues.length, 1, 'removed a cue');
+ equal(track.cues[0].startTime, 9.9, 'retained the earlier cue');
+});
+
+test('translates ID3 PTS values to cue media timeline positions', function() {
+ var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
+ player.src({
+ src: 'manifest/media.m3u8',
+ type: 'application/vnd.apple.mpegurl'
+ });
+ openMediaSource(player);
+
+ player.hls.segmentParser_.getNextTag = function() {
+ return tags.shift();
+ };
+ player.hls.segmentParser_.tagsAvailable = function() {
+ return tags.length;
+ };
+ player.hls.segmentParser_.parseSegmentBinaryData = function() {
+ // setup the timestamp offset
+ this.timestampOffset = tags[0].pts;
+
+ // fake out a descriptor
+ player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
+ 1, 2, 3, 0xbb
+ ]);
+ // trigger a metadata event
+ player.hls.segmentParser_.metadataStream.trigger('data', {
+ pts: 5 * 1000,
+ data: new Uint8Array([]),
+ frames: [{
+ id: 'TXXX',
+ value: 'cue text'
+ }]
+ });
+ };
+ standardXHRResponse(requests.shift()); // media
+ standardXHRResponse(requests.shift()); // segment 0
+
+ track = player.textTracks()[0];
+ equal(track.cues[0].startTime, 1, 'translated startTime');
+ equal(track.cues[0].endTime, 1, 'translated startTime');
+});
+
+test('translates ID3 PTS values across discontinuities', function() {
+ var tags = [], events = [], track;
+ player.src({
+ src: 'cues-and-discontinuities.m3u8',
+ type: 'application/vnd.apple.mpegurl'
+ });
+ openMediaSource(player);
+
+ player.hls.segmentParser_.getNextTag = function() {
+ return tags.shift();
+ };
+ player.hls.segmentParser_.tagsAvailable = function() {
+ return tags.length;
+ };
+ player.hls.segmentParser_.parseSegmentBinaryData = function() {
+ if (this.timestampOffset === null) {
+ this.timestampOffset = tags[0].pts;
+ }
+ // fake out a descriptor
+ player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
+ 1, 2, 3, 0xbb
+ ]);
+ // trigger a metadata event
+ if (events.length) {
+ player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
+ }
+ };
+
+ // media playlist
+ requests.shift().respond(200, null,
+ '#EXTM3U\n' +
+ '#EXTINF:10,\n' +
+ '0.ts\n' +
+ '#EXT-X-DISCONTINUITY\n' +
+ '#EXTINF:10,\n' +
+ '1.ts\n');
+
+ // segment 0 starts at PTS 14000 and has a cue point at 15000
+ tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) });
+ events.push({
+ pts: 15 * 1000,
+ data: new Uint8Array([]),
+ frames: [{
+ id: 'TXXX',
+ value: 'cue 0'
+ }]
+ });
+ standardXHRResponse(requests.shift()); // segment 0
+
+ // segment 1 is after a discontinuity, starts at PTS 22000
+ // and has a cue point at 15000
+ tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) });
+ events.push({
+ pts: 23 * 1000,
+ data: new Uint8Array([]),
+ frames: [{
+ id: 'TXXX',
+ value: 'cue 0'
+ }]
+ });
+ player.hls.checkBuffer_();
+ standardXHRResponse(requests.shift());
+
+ track = player.textTracks()[0];
+ equal(track.cues.length, 2, 'created cues');
+ equal(track.cues[0].startTime, 1, 'first cue started at the correct time');
+ equal(track.cues[0].endTime, 1, 'first cue ended at the correct time');
+ equal(track.cues[1].startTime, 11, 'second cue started at the correct time');
+ equal(track.cues[1].endTime, 11, 'second cue ended at the correct time');
+});
+
test('drops tags before the target timestamp when seeking', function() {
var i = 10,
tags = [],
@@ -1611,7 +1856,8 @@ test('clears the segment buffer on seek', function() {
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,0\n' +
- '2.ts\n');
+ '2.ts\n' +
+ '#EXT-X-ENDLIST\n');
standardXHRResponse(requests.pop());
// play to 6s to trigger the next segment request
@@ -1662,7 +1908,8 @@ test('continues playing after seek to discontinuity', function() {
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,0\n' +
- '2.ts\n');
+ '2.ts\n' +
+ '#EXT-X-ENDLIST\n');
standardXHRResponse(requests.pop());
currentTime = 1;
@@ -1751,8 +1998,7 @@ test('remove event handlers on dispose', function() {
player.dispose();
- ok(offhandlers > onhandlers, 'more handlers were removed than were registered');
- equal(offhandlers - onhandlers, 1, 'one handler was registered during init');
+ ok(offhandlers > onhandlers, 'removed all registered handlers');
});
test('aborts the source buffer on disposal', function() {