
8 changed files with 652 additions and 56 deletions
-
2CHANGELOG.md
-
1example.html
-
4src/playlist-loader.js
-
127src/videojs-hls.js
-
164test/stats/index.html
-
28test/stats/stats.css
-
238test/stats/stats.js
-
144test/videojs-hls_test.js
@ -0,0 +1,164 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<title>video.js HLS Stats</title> |
|||
|
|||
<link href="../../node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet"> |
|||
|
|||
<!-- video.js --> |
|||
<script src="../../node_modules/video.js/dist/video-js/video.dev.js"></script> |
|||
|
|||
<!-- Media Sources plugin --> |
|||
<script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> |
|||
|
|||
<!-- HLS plugin --> |
|||
<script src="../../src/videojs-hls.js"></script> |
|||
|
|||
<!-- segment handling --> |
|||
<script src="../../src/xhr.js"></script> |
|||
<script src="../../src/flv-tag.js"></script> |
|||
<script src="../../src/stream.js"></script> |
|||
<script src="../../src/exp-golomb.js"></script> |
|||
<script src="../../src/h264-extradata.js"></script> |
|||
<script src="../../src/h264-stream.js"></script> |
|||
<script src="../../src/aac-stream.js"></script> |
|||
<script src="../../src/metadata-stream.js"></script> |
|||
<script src="../../src/segment-parser.js"></script> |
|||
|
|||
<!-- 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> |
|||
|
|||
|
|||
<!-- player stats visualization --> |
|||
<link href="stats.css" rel="stylesheet"> |
|||
<script src="../switcher/js/vendor/d3.min.js"></script> |
|||
|
|||
<!-- debugging --> |
|||
<script src="../../src/bin-utils.js"></script> |
|||
<style> |
|||
body { |
|||
font-family: Arial, sans-serif; |
|||
margin: 20px; |
|||
} |
|||
.info { |
|||
background-color: #eee; |
|||
border: thin solid #333; |
|||
border-radius: 3px; |
|||
padding: 0 5px; |
|||
margin: 20px 0; |
|||
} |
|||
</style> |
|||
|
|||
</head> |
|||
<body> |
|||
<div class="info"> |
|||
<p>The video below is an <a href="https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1">HTTP Live Stream</a>. On desktop browsers other than Safari, the HLS plugin will polyfill support for the format on top of the video.js Flash tech.</p> |
|||
<p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p> |
|||
</div> |
|||
<video id="video" |
|||
class="video-js vjs-default-skin" |
|||
height="300" |
|||
width="600" |
|||
controls> |
|||
<source |
|||
src="http://s3.amazonaws.com/_bc_dml/example-content/bipbop-id3/index.m3u8" |
|||
type="application/x-mpegURL"> |
|||
</video> |
|||
<section class="stats"> |
|||
<h2>Player Stats</h2> |
|||
<div class="segment-timeline"></div> |
|||
<dl> |
|||
<dt>Current Time:</dt> |
|||
<dd class="current-time-stat">0</dd> |
|||
<dt>Buffered:</dt> |
|||
<dd><span class="buffered-start-stat">-</span> - <span class="buffered-end-stat">-</span></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 class="switching-stats"> |
|||
Once the player begins loading, you'll see information about the |
|||
operation of the adaptive quality switching here. |
|||
</div> |
|||
</section> |
|||
|
|||
<script src="stats.js"></script> |
|||
<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 bufferedStartStat = document.querySelector('.buffered-start-stat'); |
|||
var bufferedEndStat = document.querySelector('.buffered-end-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); |
|||
}); |
|||
|
|||
player.on('progress', function() { |
|||
var oldStart, oldEnd; |
|||
// buffered |
|||
var buffered = player.buffered(); |
|||
if (buffered.length) { |
|||
|
|||
oldStart = bufferedStartStat.textContent; |
|||
if (buffered.start(0).toFixed(1) !== oldStart) { |
|||
bufferedStartStat.textContent = buffered.start(0).toFixed(1); |
|||
} |
|||
oldEnd = bufferedEndStat.textContent; |
|||
if (buffered.end(0).toFixed(1) !== oldEnd) { |
|||
bufferedEndStat.textContent = buffered.end(0).toFixed(1); |
|||
} |
|||
} |
|||
|
|||
// 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); |
|||
} |
|||
} |
|||
|
|||
// bitrates |
|||
var playlist = player.hls.playlists.media(); |
|||
if (playlist && playlist.attributes && playlist.attributes.BANDWIDTH) { |
|||
videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, { |
|||
maximumFractionDigits: 1 |
|||
}) + ' kbps'; |
|||
} |
|||
if (player.hls.bandwidth) { |
|||
measuredBitrateStat.textContent = (player.hls.bandwidth / 1024).toLocaleString(undefined, { |
|||
maximumFractionDigits: 1 |
|||
}) + ' kbps'; |
|||
} |
|||
}); |
|||
|
|||
videojs.Hls.displayStats(document.querySelector('.switching-stats'), player); |
|||
videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,28 @@ |
|||
.axis text, |
|||
.cue text { |
|||
font: 12px sans-serif; |
|||
} |
|||
|
|||
.axis line, |
|||
.axis path, |
|||
.intersect { |
|||
fill: none; |
|||
stroke: #000; |
|||
} |
|||
|
|||
.cue { |
|||
width: 20px; |
|||
height: 20px; |
|||
} |
|||
.cue text { |
|||
display: none; |
|||
} |
|||
.cue:hover text { |
|||
display: block; |
|||
} |
|||
|
|||
.intersect { |
|||
fill: none; |
|||
stroke: #000; |
|||
stroke-dasharray: 2,2; |
|||
} |
@ -0,0 +1,238 @@ |
|||
(function(window, videojs, undefined) { |
|||
'use strict'; |
|||
|
|||
// -------------
|
|||
// Initial Setup
|
|||
// -------------
|
|||
|
|||
var d3 = window.d3; |
|||
|
|||
var setupGraph = function(element) { |
|||
element.innerHTML = ''; |
|||
|
|||
// setup the display
|
|||
var margin = { |
|||
top: 20, |
|||
right: 80, |
|||
bottom: 30, |
|||
left: 50 |
|||
}; |
|||
var width = 600 - margin.left - margin.right; |
|||
var height = 300 - margin.top - margin.bottom; |
|||
var svg = d3.select(element) |
|||
.append('svg') |
|||
.attr('width', width + margin.left + margin.right) |
|||
.attr('height', height + margin.top + margin.bottom) |
|||
.append('g') |
|||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
|||
|
|||
// setup the timeline
|
|||
var x = d3.time.scale().range([0, width]); // d3.scale.linear().range([0, width]);
|
|||
var y = d3.scale.linear().range([height, 0]); |
|||
|
|||
x.domain([new Date(), new Date(Date.now() + (5 * 60 * 1000))]); |
|||
y.domain([0, 5 * 1024 * 1024 * 8]); |
|||
|
|||
var timeAxis = d3.svg.axis().scale(x).orient('bottom'); |
|||
var tickFormatter = d3.format(',.0f'); |
|||
var bitrateAxis = d3.svg.axis() |
|||
.scale(y) |
|||
.tickFormat(function(value) { |
|||
return tickFormatter(value / 1024); |
|||
}) |
|||
.orient('left'); |
|||
|
|||
// time axis
|
|||
svg.selectAll('.axis').remove(); |
|||
svg.append('g') |
|||
.attr('class', 'x axis') |
|||
.attr('transform', 'translate(0,' + height + ')') |
|||
.call(timeAxis); |
|||
|
|||
// bitrate axis
|
|||
svg.append('g') |
|||
.attr('class', 'y axis') |
|||
.call(bitrateAxis) |
|||
.append('text') |
|||
.attr('transform', 'rotate(-90)') |
|||
.attr('y', 6) |
|||
.attr('dy', '.71em') |
|||
.style('text-anchor', 'end') |
|||
.text('Bitrate (kb/s)'); |
|||
|
|||
}; |
|||
|
|||
// ---------------
|
|||
// Dynamic Updates
|
|||
// ---------------
|
|||
|
|||
var displayStats = function(element, player) { |
|||
setupGraph(element, player); |
|||
}; |
|||
|
|||
// -----------------
|
|||
// Cue Visualization
|
|||
// -----------------
|
|||
|
|||
var Playlist = videojs.Hls.Playlist; |
|||
var margin = { |
|||
top: 8, |
|||
right: 8, |
|||
bottom: 20, |
|||
left: 80 |
|||
}; |
|||
var width = 600 - margin.left - margin.right; |
|||
var height = 600 - margin.top - margin.bottom; |
|||
|
|||
var mediaDomain = function(media, player) { |
|||
var segments = media.segments; |
|||
var end = player.hls.playlists.expiredPreDiscontinuity_; |
|||
end += player.hls.playlists.expiredPostDiscontinuity_; |
|||
end += Playlist.duration(media, |
|||
media.mediaSequence, |
|||
media.mediaSequence + segments.length); |
|||
return [0, end]; |
|||
}; |
|||
var ptsDomain = function(segments, mediaScale, mediaOffset) { |
|||
mediaOffset = mediaOffset * 1000 || 0; |
|||
var start = mediaScale.domain()[0] * 1000; |
|||
var segment = segments[0]; |
|||
|
|||
if (segment && |
|||
segment.minAudioPts !== undefined || |
|||
segment.minVideoPts !== undefined) { |
|||
start = Math.min(segment.minAudioPts || Infinity, |
|||
segment.minVideoPts || Infinity); |
|||
} |
|||
start -= mediaOffset; |
|||
return [ |
|||
start, |
|||
(mediaScale.domain()[1] - mediaScale.domain()[0]) * 1000 + start |
|||
]; |
|||
}; |
|||
var svgUpdateCues = function(svg, mediaScale, ptsScale, y, cues) { |
|||
cues = Array.prototype.slice.call(cues).filter(function(cue) { |
|||
return cue.startTime > mediaScale.domain()[0] && |
|||
cue.startTime < mediaScale.domain()[1]; |
|||
}); |
|||
var points = svg.selectAll('.cue').data(cues, function(cue) { |
|||
return cue.pts_ + ' -> ' + cue.startTime; |
|||
}); |
|||
points.attr('transform', function(cue) { |
|||
return 'translate(' + mediaScale(cue.startTime) + ',' + ptsScale(cue.pts_) + ')'; |
|||
}); |
|||
var enter = points.enter().append('g') |
|||
.attr('class', 'cue'); |
|||
enter.append('circle') |
|||
.attr('r', 5) |
|||
.attr('data-time', function(cue) { |
|||
return cue.startTime; |
|||
}) |
|||
.attr('data-pts', function(cue) { |
|||
return cue.pts_; |
|||
}); |
|||
enter.append('text') |
|||
.attr('transform', 'translate(8,0)') |
|||
.text(function(cue) { |
|||
return 'time: ' + videojs.formatTime(cue.startTime); |
|||
}); |
|||
enter.append('text') |
|||
.attr('transform', 'translate(8,16)') |
|||
.text(function(cue) { |
|||
return 'pts: ' + cue.pts_; |
|||
}); |
|||
points.exit().remove(); |
|||
}; |
|||
var svgUpdateAxes = function(svg, mediaScale, ptsScale) { |
|||
// media timeline axis
|
|||
var mediaAxis = d3.svg.axis().scale(mediaScale).orient('bottom'); |
|||
svg.select('.axis.media') |
|||
.transition().duration(500) |
|||
.call(mediaAxis); |
|||
|
|||
// presentation timeline axis
|
|||
if (!isFinite(ptsScale.domain()[0]) || !isFinite(ptsScale.domain()[1])) { |
|||
return; |
|||
} |
|||
var ptsAxis = d3.svg.axis().scale(ptsScale).orient('left'); |
|||
svg.select('.axis.presentation') |
|||
.transition().duration(500) |
|||
.call(ptsAxis); |
|||
}; |
|||
var svgRenderSegmentTimeline = function(container, player) { |
|||
var media = player.hls.playlists.media(); |
|||
var segments = media.segments; // media.segments.slice(0, count);
|
|||
|
|||
// setup the display
|
|||
var svg = d3.select(container) |
|||
.append('svg') |
|||
.attr('width', width + margin.left + margin.right) |
|||
.attr('height', height + margin.top + margin.bottom) |
|||
.append('g') |
|||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
|||
|
|||
// setup the scales
|
|||
var mediaScale = d3.scale.linear().range([0, width]); |
|||
mediaScale.domain(mediaDomain(media, player)); |
|||
var ptsScale = d3.scale.linear().range([height, 0]); |
|||
ptsScale.domain(ptsDomain(segments, mediaScale)); |
|||
|
|||
// render
|
|||
var mediaAxis = d3.svg.axis().scale(mediaScale).orient('bottom'); |
|||
svg.append('g') |
|||
.attr('class', 'x axis media') |
|||
.attr('transform', 'translate(0,' + height + ')') |
|||
.call(mediaAxis); |
|||
var ptsAxis = d3.svg.axis().scale(ptsScale).orient('left'); |
|||
svg.append('g') |
|||
.attr('class', 'y axis presentation') |
|||
.call(ptsAxis); |
|||
|
|||
svg.append('path') |
|||
.attr('class', 'intersect') |
|||
.attr('d', 'M0,' + height + 'L' + width +',0'); |
|||
|
|||
var mediaOffset = 0; |
|||
|
|||
// update everything on progress
|
|||
player.on('progress', function() { |
|||
var updatedMedia = player.hls.playlists.media(); |
|||
var segments = updatedMedia.segments; // updatedMedia.segments.slice(currentIndex, currentIndex + count);
|
|||
|
|||
if (updatedMedia.mediaSequence !== media.mediaSequence) { |
|||
mediaOffset += Playlist.duration(media, |
|||
media.mediaSequence, |
|||
updatedMedia.mediaSequence); |
|||
media = updatedMedia; |
|||
} |
|||
|
|||
mediaScale.domain(mediaDomain(updatedMedia, player)); |
|||
ptsScale.domain(ptsDomain(segments, mediaScale, mediaOffset)); |
|||
svgUpdateAxes(svg, mediaScale, ptsScale, updatedMedia, segments); |
|||
if (!isFinite(ptsScale.domain()[0]) || !isFinite(ptsScale.domain()[1])) { |
|||
return; |
|||
} |
|||
for (var i = 0; i < player.textTracks().length; i++) { |
|||
var track = player.textTracks()[i]; |
|||
svgUpdateCues(svg, mediaScale, ptsScale, ptsScale, track.cues); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
var displayCues = function(container, player) { |
|||
var media = player.hls.playlists.media(); |
|||
if (media && media.segments) { |
|||
svgRenderSegmentTimeline(container, player); |
|||
} else { |
|||
player.one('loadedmetadata', function() { |
|||
svgRenderSegmentTimeline(container, player); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
|
|||
// export
|
|||
videojs.Hls.displayStats = displayStats; |
|||
videojs.Hls.displayCues = displayCues; |
|||
|
|||
})(window, window.videojs); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue