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