Skip to content
This repository has been archived by the owner on Jan 12, 2019. It is now read-only.

Commit

Permalink
@dmlap implement seekable for live streams ([view](#295))
Browse files Browse the repository at this point in the history
  • Loading branch information
dmlap committed Jun 5, 2015
2 parents c4a8ac2 + e1e725c commit 0cd39e1
Show file tree
Hide file tree
Showing 57 changed files with 1,081 additions and 302 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

--------------------

Expand Down
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion example.html
Original file line number Diff line number Diff line change
Expand Up @@ -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> -->
Expand Down
4 changes: 1 addition & 3 deletions src/aac-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ var
window.videojs.Hls.AacStream = function() {
var
next_pts, // :uint
pts_offset, // :int
state, // :uint
pes_length, // :int
lastMetaPts,
Expand All @@ -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
Expand All @@ -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
Expand Down
9 changes: 3 additions & 6 deletions src/h264-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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) {
Expand Down
29 changes: 28 additions & 1 deletion src/m3u8/m3u8-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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', {
Expand Down
10 changes: 2 additions & 8 deletions src/metadata-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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 {
Expand Down
168 changes: 119 additions & 49 deletions src/playlist-loader.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -141,7 +170,7 @@
var mediaChange = false;
// getter
if (!playlist) {
return media;
return loader.media_;
}

// setter
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Loading

0 comments on commit 0cd39e1

Please sign in to comment.