cd17637b by David LaPalomento

Move stats out to a dedicated page

Keep the example page simple. Add a live graph of cue point PTS position versus media timeline position. Update the display so it works with live content.
1 parent 2b7c3792
......@@ -71,86 +71,11 @@
src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
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><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>
</section>
<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.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';
}
});
</script>
</body>
</html>
......
......@@ -329,6 +329,7 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
time = segmentOffset + ((metadata.pts - minPts) * 0.001);
cue = new window.VTTCue(time, time, frame.value || frame.url || '');
cue.frame = frame;
cue.pts_ = metadata.pts;
textTrack.addCue(cue);
}
segmentInfo.pendingMetadata.shift();
......
<!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>
.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;
}
(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);