0cd39e1c by David LaPalomento

@dmlap implement seekable for live streams ([view](https://github.com/videojs/vi…

…deojs-contrib-hls/pull/295))
2 parents c4a8ac25 e1e725cd
Showing 57 changed files with 1081 additions and 302 deletions
......@@ -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))
--------------------
......
......@@ -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'
......
......@@ -28,13 +28,14 @@
<!-- m3u8 handling -->
<script src="src/m3u8/m3u8-parser.js"></script>
<script src="src/playlist.js"></script>
<script src="src/playlist-loader.js"></script>
<script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="src/decrypter.js"></script>
<script src="src/bin-utils.js"></script>
<!-- example MPEG2-TS segments -->
<!-- bipbop -->
<!-- <script src="test/tsSegment.js"></script> -->
......
......@@ -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
......
......@@ -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) {
......
......@@ -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', {
......
......@@ -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 {
......
/**
* 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);
......
/**
* 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);
......@@ -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);
......
......@@ -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);
};
/**
......
......@@ -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,
......
......@@ -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',
......
......@@ -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',
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -137,5 +137,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
{
"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
}
#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
{
"allowCache": true,
"mediaSequence": 0,
"discontinuitySequence": 0,
"segments": [
{
"duration": 10,
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -28,5 +28,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
{
"allowCache": true,
"mediaSequence": 7794,
"discontinuitySequence": 0,
"segments": [
{
"duration": 2.833,
......
......@@ -29,5 +29,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -8,5 +8,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -28,5 +28,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -140,5 +140,6 @@
"uri": "hls_450k_video.ts"
}
],
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -16,5 +16,6 @@
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"discontinuitySequence": 0
}
......
......@@ -39,5 +39,6 @@
"uri": "009.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
"targetDuration": 10,
"discontinuitySequence": 0
}
......
......@@ -7,5 +7,6 @@
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
],
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -24,5 +24,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -8,5 +8,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -11,5 +11,6 @@
"uri": "00002.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"discontinuitySequence": 0
}
......
......@@ -17,5 +17,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -18,5 +18,6 @@
"uri": "004.ts",
"duration": 10
}
]
],
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -9,5 +9,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -9,5 +9,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -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]))
});
......
......@@ -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);
......
/* 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);
......@@ -15,7 +15,7 @@
<script src="../libs/qunit/qunit.js"></script>
<!-- video.js -->
<script src="../node_modules/video.js/dist/video-js/video.js"></script>
<script src="../node_modules/video.js/dist/video-js/video.dev.js"></script>
<script src="../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<!-- HLS plugin -->
......@@ -32,6 +32,7 @@
<!-- M3U8 -->
<script src="../src/m3u8/m3u8-parser.js"></script>
<script src="../src/playlist.js"></script>
<script src="../src/playlist-loader.js"></script>
<script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="../src/decrypter.js"></script>
......@@ -59,6 +60,7 @@
<script src="flv-tag_test.js"></script>
<script src="metadata-stream_test.js"></script>
<script src="m3u8_test.js"></script>
<script src="playlist_test.js"></script>
<script src="playlist-loader_test.js"></script>
<script src="decrypter_test.js"></script>
<script src="xhr_test.js"></script>
......
......@@ -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() {
......