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
aecdda0d
authored
2014-06-30 10:20:27 -0400
by
David LaPalomento
Browse Files
Options
Browse Files
Tag
Download
Plain Diff
Merge pull request #92 from videojs/hotfix/bw-scenarios
Adaptive switching enhancements
2 parents
64b2d4f3
ba1507ee
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
159 additions
and
34 deletions
src/videojs-hls.js
test/switcher/css/main.css
test/switcher/index.html
test/switcher/js/switcher.js
test/videojs-hls_test.js
src/videojs-hls.js
View file @
aecdda0
...
...
@@ -11,9 +11,6 @@
var
// the desired length of video to maintain in the buffer, in seconds
goalBufferLength
=
5
,
// a fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance
=
1.1
,
...
...
@@ -333,6 +330,20 @@ var
};
/**
* Abort all outstanding work and cleanup.
*/
player
.
hls
.
dispose
=
function
()
{
if
(
segmentXhr
)
{
segmentXhr
.
onreadystatechange
=
null
;
segmentXhr
.
abort
();
}
if
(
this
.
playlists
)
{
this
.
playlists
.
dispose
();
}
videojs
.
Flash
.
prototype
.
dispose
.
call
(
this
);
};
/**
* 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 offset (optional) {number} the offset into the downloaded segment
...
...
@@ -370,7 +381,8 @@ var
// if there is plenty of content in the buffer and we're not
// seeking, relax for awhile
if
(
typeof
offset
!==
'number'
&&
bufferedTime
>=
goalBufferLength
)
{
if
(
typeof
offset
!==
'number'
&&
bufferedTime
>=
videojs
.
Hls
.
GOAL_BUFFER_LENGTH
)
{
return
;
}
...
...
@@ -396,6 +408,12 @@ var
segmentXhr
=
null
;
if
(
error
)
{
// if a segment request times out, we may have better luck with another playlist
if
(
error
===
'timeout'
)
{
player
.
hls
.
bandwidth
=
1
;
return
player
.
hls
.
playlists
.
media
(
player
.
hls
.
selectPlaylist
());
}
// otherwise, try jumping ahead to the next segment
player
.
hls
.
error
=
{
status
:
this
.
status
,
message
:
'HLS segment request error at URL: '
+
url
,
...
...
@@ -577,6 +595,9 @@ videojs.Hls = videojs.Flash.extend({
}
});
// the desired length of video to maintain in the buffer, in seconds
videojs
.
Hls
.
GOAL_BUFFER_LENGTH
=
30
;
videojs
.
Hls
.
prototype
.
src
=
function
(
src
)
{
var
player
=
this
.
player
(),
...
...
@@ -606,13 +627,6 @@ videojs.Hls.prototype.duration = function() {
return
0
;
};
videojs
.
Hls
.
prototype
.
dispose
=
function
()
{
if
(
this
.
playlists
)
{
this
.
playlists
.
dispose
();
}
videojs
.
Flash
.
prototype
.
dispose
.
call
(
this
);
};
videojs
.
Hls
.
isSupported
=
function
()
{
return
videojs
.
Flash
.
isSupported
()
&&
videojs
.
MediaSource
;
};
...
...
@@ -664,10 +678,14 @@ xhr = videojs.Hls.xhr = function(url, callback) {
if
(
options
.
timeout
)
{
if
(
request
.
timeout
===
0
)
{
request
.
timeout
=
options
.
timeout
;
request
.
ontimeout
=
function
()
{
request
.
timedout
=
true
;
};
}
else
{
// polyfill XHR2 by aborting after the timeout
abortTimeout
=
window
.
setTimeout
(
function
()
{
if
(
request
.
readystate
!==
4
)
{
request
.
timedout
=
true
;
request
.
abort
();
}
},
options
.
timeout
);
...
...
@@ -683,7 +701,12 @@ xhr = videojs.Hls.xhr = function(url, callback) {
// clear outstanding timeouts
window
.
clearTimeout
(
abortTimeout
);
// request error
// request timeout
if
(
request
.
timedout
)
{
return
callback
.
call
(
this
,
'timeout'
,
url
);
}
// request aborted or errored
if
(
this
.
status
>=
400
||
this
.
status
===
0
)
{
return
callback
.
call
(
this
,
true
,
url
);
}
...
...
test/switcher/css/main.css
View file @
aecdda0
...
...
@@ -432,6 +432,11 @@ form label {
stroke-dasharray
:
5
,
5
;
}
.buffer-empty
{
fill
:
#e44d26
;
opacity
:
0.4
;
}
.timeline
{
color
:
#888
;
height
:
500px
;
...
...
test/switcher/index.html
View file @
aecdda0
...
...
@@ -85,6 +85,13 @@
capacity.
<button
type=
button
class=
add-time-period
>
Add time period
</button>
</p>
<p>
If you've created a complex scenario you'd like to retry
later, run the simulation and then save the URL of this
page. All of the network conditions you specify are
saved into the URL fragment after the results of a
simulation run are displayed.
</p>
The video is available at
<ul>
<li><input
class=
bitrate
type=
number
min=
1
value=
65536
>
bits per second
</li>
...
...
test/switcher/js/switcher.js
View file @
aecdda0
...
...
@@ -10,9 +10,6 @@
player
,
runButton
,
parameters
,
addTimePeriod
,
networkTimeline
,
timePeriod
,
timeline
,
displayTimeline
;
...
...
@@ -31,24 +28,47 @@
};
// a dynamic number of time-bandwidth pairs may be defined to drive the simulation
addTimePeriod
=
document
.
querySelector
(
'.add-time-period'
);
networkTimeline
=
document
.
querySelector
(
'.network-timeline'
);
timePeriod
=
networkTimeline
.
cloneNode
(
true
);
addTimePeriod
.
addEventListener
(
'click'
,
function
()
{
(
function
()
{
var
params
,
networkTimeline
=
document
.
querySelector
(
'.network-timeline'
),
timePeriod
=
networkTimeline
.
querySelector
(
'li:last-child'
).
cloneNode
(
true
),
appendTimePeriod
=
function
()
{
var
clone
=
timePeriod
.
cloneNode
(
true
),
fragment
=
document
.
createDocumentFragment
(),
count
=
networkTimeline
.
querySelectorAll
(
'input.bandwidth'
).
length
,
time
=
clone
.
querySelector
(
'.time'
),
bandwidth
=
clone
.
querySelector
(
'input.bandwidth'
);
time
.
name
=
'time'
+
count
;
bandwidth
.
name
=
'bandwidth'
+
count
;
while
(
clone
.
childNodes
.
length
)
{
fragment
.
appendChild
(
clone
.
childNodes
[
0
]);
networkTimeline
.
appendChild
(
clone
);
};
document
.
querySelector
(
'.add-time-period'
)
.
addEventListener
(
'click'
,
appendTimePeriod
);
// apply any simulation parameters that were set in the fragment identifier
if
(
!
window
.
location
.
hash
)
{
return
;
}
networkTimeline
.
appendChild
(
fragment
);
// time periods are specified as t<seconds>=<bitrate>
// e.g. #t15=450560&t150=65530
params
=
window
.
location
.
hash
.
substring
(
1
)
.
split
(
'&'
)
.
map
(
function
(
param
)
{
return
((
/t
(\d
+
)
=
(\d
+
)
/i
).
exec
(
param
)
||
[])
.
map
(
window
.
parseFloat
).
slice
(
1
);
}).
filter
(
function
(
pair
)
{
return
pair
.
length
===
2
;
});
networkTimeline
.
innerHTML
=
''
;
params
.
forEach
(
function
(
param
)
{
appendTimePeriod
();
networkTimeline
.
querySelector
(
'li:last-child .time'
).
value
=
param
[
0
];
networkTimeline
.
querySelector
(
'li:last-child input.bandwidth'
).
value
=
param
[
1
];
});
})();
// collect the simulation parameters
parameters
=
function
()
{
var
times
=
Array
.
prototype
.
slice
.
call
(
document
.
querySelectorAll
(
'.time'
)),
...
...
@@ -143,7 +163,7 @@
buffered
=
0
,
currentTime
=
0
;
//
mock out buffered and currentTime
//
simulate buffered and currentTime during playback
player
.
buffered
=
function
()
{
return
videojs
.
createTimeRange
(
0
,
currentTime
+
buffered
);
};
...
...
@@ -191,10 +211,6 @@
// segment response headers arrive after the propogation delay
setTimeout
(
function
()
{
var
arrival
=
Math
.
ceil
(
+
new
Date
()
*
0.001
);
results
.
playlists
.
push
({
time
:
arrival
,
bitrate
:
+
request
.
url
.
match
(
/
(\d
+
)
-
\d
+$/
)[
1
]
});
request
.
setResponseHeaders
({
'Content-Type'
:
'video/mp2t'
});
...
...
@@ -204,12 +220,23 @@
if
(
remaining
-
value
.
bandwidth
<=
0
)
{
// send the response body once all bytes have been delivered
setTimeout
(
function
()
{
buffered
+=
segmentDuration
;
var
time
=
Math
.
ceil
(
+
new
Date
()
*
0.001
);
if
(
request
.
aborted
)
{
return
;
}
request
.
status
=
200
;
request
.
response
=
new
Uint8Array
(
segmentSize
*
0.125
);
request
.
setResponseBody
(
''
);
results
.
playlists
.
push
({
time
:
time
,
bitrate
:
+
request
.
url
.
match
(
/
(\d
+
)
-
\d
+$/
)[
1
]
});
// update the buffered value
buffered
+=
segmentDuration
;
results
.
buffered
[
results
.
buffered
.
length
-
1
].
buffered
=
buffered
;
results
.
effectiveBandwidth
.
push
({
time
:
Math
.
ceil
(
+
new
Date
()
*
0.001
)
,
time
:
time
,
bandwidth
:
player
.
hls
.
bandwidth
});
},
((
remaining
/
value
.
bandwidth
)
+
i
)
*
1000
);
...
...
@@ -240,6 +267,11 @@
clock
.
restore
();
fakeXhr
.
restore
();
// update the fragment identifier so this scenario can be re-run easily
window
.
location
.
hash
=
'#'
+
options
.
bandwidths
.
map
(
function
(
interval
)
{
return
't'
+
interval
.
time
+
'='
+
interval
.
bandwidth
;
}).
join
(
'&'
);
done
(
null
,
results
);
},
0
);
});
...
...
@@ -303,7 +335,9 @@
}));
y
.
domain
([
0
,
Math
.
max
(
d3
.
max
(
data
.
bandwidth
,
function
(
data
)
{
return
data
.
bandwidth
;
}),
d3
.
max
(
data
.
options
.
playlists
))]);
}),
d3
.
max
(
data
.
options
.
playlists
),
d3
.
max
(
data
.
playlists
,
function
(
data
)
{
return
data
.
bitrate
;
}))]);
// time axis
svg
.
selectAll
(
'.axis'
).
remove
();
...
...
@@ -324,6 +358,7 @@
.
text
(
'Bitrate (kb/s)'
);
// playlist bitrate lines
svg
.
selectAll
(
'.line.bitrate'
).
remove
();
svg
.
selectAll
(
'.line.bitrate'
)
.
data
(
data
.
options
.
playlists
)
.
enter
().
append
(
'path'
)
...
...
@@ -368,6 +403,40 @@
.
attr
(
'cy'
,
function
(
playlist
)
{
return
y
(
playlist
.
bitrate
);
});
// highlight intervals when the buffer is empty
svg
.
selectAll
(
'.buffer-empty'
).
remove
();
svg
.
selectAll
(
'.buffer-empty'
)
.
data
(
data
.
buffered
.
reduce
(
function
(
result
,
sample
)
{
var
last
=
result
[
result
.
length
-
1
];
if
(
sample
.
buffered
===
0
)
{
if
(
last
&&
sample
.
time
===
last
.
end
+
1
)
{
// add this sample to the interval we're accumulating
return
result
.
slice
(
0
,
result
.
length
-
1
).
concat
({
start
:
last
.
start
,
end
:
sample
.
time
});
}
else
{
// this sample starts a new interval
return
result
.
concat
({
start
:
sample
.
time
,
end
:
sample
.
time
});
}
}
// filter out time periods where the buffer isn't empty
return
result
;
},
[]))
.
enter
().
append
(
'rect'
)
.
attr
(
'class'
,
'buffer-empty'
)
.
attr
(
'x'
,
function
(
data
)
{
return
x
(
data
.
start
);
})
.
attr
(
'width'
,
function
(
data
)
{
return
x
(
1
+
data
.
end
-
data
.
start
);
})
.
attr
(
'y'
,
0
)
.
attr
(
'height'
,
y
(
height
));
};
})();
...
...
test/videojs-hls_test.js
View file @
aecdda0
...
...
@@ -420,7 +420,6 @@ test('selects a playlist after segment downloads', function() {
player
.
trigger
(
'timeupdate'
);
standardXHRResponse
(
requests
[
3
]);
console
.
log
(
requests
.
map
(
function
(
i
)
{
return
i
.
url
;
}));
strictEqual
(
calls
,
2
,
'selects after additional segments'
);
});
...
...
@@ -631,15 +630,16 @@ test('selects the correct rendition by player dimensions', function() {
test
(
'does not download the next segment if the buffer is full'
,
function
()
{
var
currentTime
=
15
;
player
.
src
({
src
:
'manifest/media.m3u8'
,
type
:
'application/vnd.apple.mpegurl'
});
player
.
currentTime
=
function
()
{
return
15
;
return
currentTime
;
};
player
.
buffered
=
function
()
{
return
videojs
.
createTimeRange
(
0
,
20
);
return
videojs
.
createTimeRange
(
0
,
currentTime
+
videojs
.
Hls
.
GOAL_BUFFER_LENGTH
);
};
player
.
hls
.
mediaSource
.
trigger
({
type
:
'sourceopen'
...
...
@@ -1150,6 +1150,27 @@ test('clears the segment buffer on seek', function() {
strictEqual
(
aborts
,
1
,
'cleared the segment buffer on a seek'
);
});
test
(
'resets the switching algorithm if a request times out'
,
function
()
{
player
.
src
({
src
:
'master.m3u8'
,
type
:
'application/vnd.apple.mpegurl'
});
player
.
hls
.
mediaSource
.
trigger
({
type
:
'sourceopen'
});
standardXHRResponse
(
requests
.
shift
());
// master
standardXHRResponse
(
requests
.
shift
());
// media.m3u8
// simulate a segment timeout
requests
[
0
].
timedout
=
true
;
requests
.
shift
().
abort
();
standardXHRResponse
(
requests
.
shift
());
strictEqual
(
player
.
hls
.
playlists
.
media
(),
player
.
hls
.
playlists
.
master
.
playlists
[
1
],
'reset to the lowest bitrate playlist'
);
});
test
(
'disposes the playlist loader'
,
function
()
{
var
disposes
=
0
,
player
,
loaderDispose
;
player
=
createPlayer
();
...
...
Please
register
or
sign in
to post a comment