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 @@ ...@@ -71,86 +71,11 @@
71 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8" 71 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
72 type="application/x-mpegURL"> 72 type="application/x-mpegURL">
73 </video> 73 </video>
74 <section class="stats">
75 <h2>Player Stats</h2>
76 <dl>
77 <dt>Current Time:</dt>
78 <dd class="current-time-stat">0</dd>
79 <dt>Buffered:</dt>
80 <dd><span class="buffered-start-stat">-</span> - <span class="buffered-end-stat">-</span></dd>
81 <dt>Seekable:</dt>
82 <dd><span class="seekable-start-stat">-</span> - <span class="seekable-end-stat">-</span></dd>
83 <dt>Video Bitrate:</dt>
84 <dd class="video-bitrate-stat">0 kbps</dd>
85 <dt>Measured Bitrate:</dt>
86 <dd class="measured-bitrate-stat">0 kbps</dd>
87 </dl>
88 </section>
89 <script> 74 <script>
90 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf'; 75 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
76
91 // initialize the player 77 // initialize the player
92 var player = videojs('video'); 78 var player = videojs('video');
93
94 // ------------
95 // Player Stats
96 // ------------
97
98 var currentTimeStat = document.querySelector('.current-time-stat');
99 var bufferedStartStat = document.querySelector('.buffered-start-stat');
100 var bufferedEndStat = document.querySelector('.buffered-end-stat');
101 var seekableStartStat = document.querySelector('.seekable-start-stat');
102 var seekableEndStat = document.querySelector('.seekable-end-stat');
103 var videoBitrateState = document.querySelector('.video-bitrate-stat');
104 var measuredBitrateStat = document.querySelector('.measured-bitrate-stat');
105
106
107 player.on('timeupdate', function() {
108 currentTimeStat.textContent = player.currentTime().toFixed(1);
109 });
110
111 player.on('progress', function() {
112 var oldStart, oldEnd;
113 // buffered
114 var buffered = player.buffered();
115 if (buffered.length) {
116
117 oldStart = bufferedStartStat.textContent;
118 if (buffered.start(0).toFixed(1) !== oldStart) {
119 bufferedStartStat.textContent = buffered.start(0).toFixed(1);
120 }
121 oldEnd = bufferedEndStat.textContent;
122 if (buffered.end(0).toFixed(1) !== oldEnd) {
123 bufferedEndStat.textContent = buffered.end(0).toFixed(1);
124 }
125 }
126
127 // seekable
128 var seekable = player.seekable();
129 if (seekable && seekable.length) {
130
131 oldStart = seekableStartStat.textContent;
132 if (seekable.start(0).toFixed(1) !== oldStart) {
133 seekableStartStat.textContent = seekable.start(0).toFixed(1);
134 }
135 oldEnd = seekableEndStat.textContent;
136 if (seekable.end(0).toFixed(1) !== oldEnd) {
137 seekableEndStat.textContent = seekable.end(0).toFixed(1);
138 }
139 }
140
141 // bitrates
142 var playlist = player.hls.playlists.media();
143 if (playlist && playlist.attributes.BANDWIDTH) {
144 videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, {
145 maximumFractionDigits: 1
146 }) + ' kbps';
147 }
148 if (player.hls.bandwidth) {
149 measuredBitrateStat.textContent = (player.hls.bandwidth / 1024).toLocaleString(undefined, {
150 maximumFractionDigits: 1
151 }) + ' kbps';
152 }
153 });
154 </script> 79 </script>
155 </body> 80 </body>
156 </html> 81 </html>
......
...@@ -329,6 +329,7 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { ...@@ -329,6 +329,7 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
329 time = segmentOffset + ((metadata.pts - minPts) * 0.001); 329 time = segmentOffset + ((metadata.pts - minPts) * 0.001);
330 cue = new window.VTTCue(time, time, frame.value || frame.url || ''); 330 cue = new window.VTTCue(time, time, frame.value || frame.url || '');
331 cue.frame = frame; 331 cue.frame = frame;
332 cue.pts_ = metadata.pts;
332 textTrack.addCue(cue); 333 textTrack.addCue(cue);
333 } 334 }
334 segmentInfo.pendingMetadata.shift(); 335 segmentInfo.pendingMetadata.shift();
......
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <title>video.js HLS Stats</title>
6
7 <link href="../../node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet">
8
9 <!-- video.js -->
10 <script src="../../node_modules/video.js/dist/video-js/video.dev.js"></script>
11
12 <!-- Media Sources plugin -->
13 <script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
14
15 <!-- HLS plugin -->
16 <script src="../../src/videojs-hls.js"></script>
17
18 <!-- segment handling -->
19 <script src="../../src/xhr.js"></script>
20 <script src="../../src/flv-tag.js"></script>
21 <script src="../../src/stream.js"></script>
22 <script src="../../src/exp-golomb.js"></script>
23 <script src="../../src/h264-extradata.js"></script>
24 <script src="../../src/h264-stream.js"></script>
25 <script src="../../src/aac-stream.js"></script>
26 <script src="../../src/metadata-stream.js"></script>
27 <script src="../../src/segment-parser.js"></script>
28
29 <!-- m3u8 handling -->
30 <script src="../../src/m3u8/m3u8-parser.js"></script>
31 <script src="../../src/playlist.js"></script>
32 <script src="../../src/playlist-loader.js"></script>
33
34 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
35 <script src="../../src/decrypter.js"></script>
36
37
38 <!-- player stats visualization -->
39 <link href="stats.css" rel="stylesheet">
40 <script src="../switcher/js/vendor/d3.min.js"></script>
41
42 <!-- debugging -->
43 <script src="../../src/bin-utils.js"></script>
44 <style>
45 body {
46 font-family: Arial, sans-serif;
47 margin: 20px;
48 }
49 .info {
50 background-color: #eee;
51 border: thin solid #333;
52 border-radius: 3px;
53 padding: 0 5px;
54 margin: 20px 0;
55 }
56 </style>
57
58 </head>
59 <body>
60 <div class="info">
61 <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>
62 <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p>
63 </div>
64 <video id="video"
65 class="video-js vjs-default-skin"
66 height="300"
67 width="600"
68 controls>
69 <source
70 src="http://s3.amazonaws.com/_bc_dml/example-content/bipbop-id3/index.m3u8"
71 type="application/x-mpegURL">
72 </video>
73 <section class="stats">
74 <h2>Player Stats</h2>
75 <div class="segment-timeline"></div>
76 <dl>
77 <dt>Current Time:</dt>
78 <dd class="current-time-stat">0</dd>
79 <dt>Buffered:</dt>
80 <dd><span class="buffered-start-stat">-</span> - <span class="buffered-end-stat">-</span></dd>
81 <dt>Seekable:</dt>
82 <dd><span class="seekable-start-stat">-</span> - <span class="seekable-end-stat">-</span></dd>
83 <dt>Video Bitrate:</dt>
84 <dd class="video-bitrate-stat">0 kbps</dd>
85 <dt>Measured Bitrate:</dt>
86 <dd class="measured-bitrate-stat">0 kbps</dd>
87 </dl>
88 <div class="switching-stats">
89 Once the player begins loading, you'll see information about the
90 operation of the adaptive quality switching here.
91 </div>
92 </section>
93
94 <script src="stats.js"></script>
95 <script>
96 videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf';
97 // initialize the player
98 var player = videojs('video');
99
100 // ------------
101 // Player Stats
102 // ------------
103
104 var currentTimeStat = document.querySelector('.current-time-stat');
105 var bufferedStartStat = document.querySelector('.buffered-start-stat');
106 var bufferedEndStat = document.querySelector('.buffered-end-stat');
107 var seekableStartStat = document.querySelector('.seekable-start-stat');
108 var seekableEndStat = document.querySelector('.seekable-end-stat');
109 var videoBitrateState = document.querySelector('.video-bitrate-stat');
110 var measuredBitrateStat = document.querySelector('.measured-bitrate-stat');
111
112 player.on('timeupdate', function() {
113 currentTimeStat.textContent = player.currentTime().toFixed(1);
114 });
115
116 player.on('progress', function() {
117 var oldStart, oldEnd;
118 // buffered
119 var buffered = player.buffered();
120 if (buffered.length) {
121
122 oldStart = bufferedStartStat.textContent;
123 if (buffered.start(0).toFixed(1) !== oldStart) {
124 bufferedStartStat.textContent = buffered.start(0).toFixed(1);
125 }
126 oldEnd = bufferedEndStat.textContent;
127 if (buffered.end(0).toFixed(1) !== oldEnd) {
128 bufferedEndStat.textContent = buffered.end(0).toFixed(1);
129 }
130 }
131
132 // seekable
133 var seekable = player.seekable();
134 if (seekable && seekable.length) {
135
136 oldStart = seekableStartStat.textContent;
137 if (seekable.start(0).toFixed(1) !== oldStart) {
138 seekableStartStat.textContent = seekable.start(0).toFixed(1);
139 }
140 oldEnd = seekableEndStat.textContent;
141 if (seekable.end(0).toFixed(1) !== oldEnd) {
142 seekableEndStat.textContent = seekable.end(0).toFixed(1);
143 }
144 }
145
146 // bitrates
147 var playlist = player.hls.playlists.media();
148 if (playlist && playlist.attributes && playlist.attributes.BANDWIDTH) {
149 videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, {
150 maximumFractionDigits: 1
151 }) + ' kbps';
152 }
153 if (player.hls.bandwidth) {
154 measuredBitrateStat.textContent = (player.hls.bandwidth / 1024).toLocaleString(undefined, {
155 maximumFractionDigits: 1
156 }) + ' kbps';
157 }
158 });
159
160 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player);
161 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player);
162 </script>
163 </body>
164 </html>
1 .axis text,
2 .cue text {
3 font: 12px sans-serif;
4 }
5
6 .axis line,
7 .axis path,
8 .intersect {
9 fill: none;
10 stroke: #000;
11 }
12
13 .cue {
14 width: 20px;
15 height: 20px;
16 }
17 .cue text {
18 display: none;
19 }
20 .cue:hover text {
21 display: block;
22 }
23
24 .intersect {
25 fill: none;
26 stroke: #000;
27 stroke-dasharray: 2,2;
28 }
1 (function(window, videojs, undefined) {
2 'use strict';
3
4 // -------------
5 // Initial Setup
6 // -------------
7
8 var d3 = window.d3;
9
10 var setupGraph = function(element) {
11 element.innerHTML = '';
12
13 // setup the display
14 var margin = {
15 top: 20,
16 right: 80,
17 bottom: 30,
18 left: 50
19 };
20 var width = 600 - margin.left - margin.right;
21 var height = 300 - margin.top - margin.bottom;
22 var svg = d3.select(element)
23 .append('svg')
24 .attr('width', width + margin.left + margin.right)
25 .attr('height', height + margin.top + margin.bottom)
26 .append('g')
27 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
28
29 // setup the timeline
30 var x = d3.time.scale().range([0, width]); // d3.scale.linear().range([0, width]);
31 var y = d3.scale.linear().range([height, 0]);
32
33 x.domain([new Date(), new Date(Date.now() + (5 * 60 * 1000))]);
34 y.domain([0, 5 * 1024 * 1024 * 8]);
35
36 var timeAxis = d3.svg.axis().scale(x).orient('bottom');
37 var tickFormatter = d3.format(',.0f');
38 var bitrateAxis = d3.svg.axis()
39 .scale(y)
40 .tickFormat(function(value) {
41 return tickFormatter(value / 1024);
42 })
43 .orient('left');
44
45 // time axis
46 svg.selectAll('.axis').remove();
47 svg.append('g')
48 .attr('class', 'x axis')
49 .attr('transform', 'translate(0,' + height + ')')
50 .call(timeAxis);
51
52 // bitrate axis
53 svg.append('g')
54 .attr('class', 'y axis')
55 .call(bitrateAxis)
56 .append('text')
57 .attr('transform', 'rotate(-90)')
58 .attr('y', 6)
59 .attr('dy', '.71em')
60 .style('text-anchor', 'end')
61 .text('Bitrate (kb/s)');
62
63 };
64
65 // ---------------
66 // Dynamic Updates
67 // ---------------
68
69 var displayStats = function(element, player) {
70 setupGraph(element, player);
71 };
72
73 // -----------------
74 // Cue Visualization
75 // -----------------
76
77 var Playlist = videojs.Hls.Playlist;
78 var margin = {
79 top: 8,
80 right: 8,
81 bottom: 20,
82 left: 80
83 };
84 var width = 600 - margin.left - margin.right;
85 var height = 600 - margin.top - margin.bottom;
86
87 var mediaDomain = function(media, player) {
88 var segments = media.segments;
89 var end = player.hls.playlists.expiredPreDiscontinuity_;
90 end += player.hls.playlists.expiredPostDiscontinuity_;
91 end += Playlist.duration(media,
92 media.mediaSequence,
93 media.mediaSequence + segments.length);
94 return [0, end];
95 };
96 var ptsDomain = function(segments, mediaScale, mediaOffset) {
97 mediaOffset = mediaOffset * 1000 || 0;
98 var start = mediaScale.domain()[0] * 1000;
99 var segment = segments[0];
100
101 if (segment &&
102 segment.minAudioPts !== undefined ||
103 segment.minVideoPts !== undefined) {
104 start = Math.min(segment.minAudioPts || Infinity,
105 segment.minVideoPts || Infinity);
106 }
107 start -= mediaOffset;
108 return [
109 start,
110 (mediaScale.domain()[1] - mediaScale.domain()[0]) * 1000 + start
111 ];
112 };
113 var svgUpdateCues = function(svg, mediaScale, ptsScale, y, cues) {
114 cues = Array.prototype.slice.call(cues).filter(function(cue) {
115 return cue.startTime > mediaScale.domain()[0] &&
116 cue.startTime < mediaScale.domain()[1];
117 });
118 var points = svg.selectAll('.cue').data(cues, function(cue) {
119 return cue.pts_ + ' -> ' + cue.startTime;
120 });
121 points.attr('transform', function(cue) {
122 return 'translate(' + mediaScale(cue.startTime) + ',' + ptsScale(cue.pts_) + ')';
123 });
124 var enter = points.enter().append('g')
125 .attr('class', 'cue');
126 enter.append('circle')
127 .attr('r', 5)
128 .attr('data-time', function(cue) {
129 return cue.startTime;
130 })
131 .attr('data-pts', function(cue) {
132 return cue.pts_;
133 });
134 enter.append('text')
135 .attr('transform', 'translate(8,0)')
136 .text(function(cue) {
137 return 'time: ' + videojs.formatTime(cue.startTime);
138 });
139 enter.append('text')
140 .attr('transform', 'translate(8,16)')
141 .text(function(cue) {
142 return 'pts: ' + cue.pts_;
143 });
144 points.exit().remove();
145 };
146 var svgUpdateAxes = function(svg, mediaScale, ptsScale) {
147 // media timeline axis
148 var mediaAxis = d3.svg.axis().scale(mediaScale).orient('bottom');
149 svg.select('.axis.media')
150 .transition().duration(500)
151 .call(mediaAxis);
152
153 // presentation timeline axis
154 if (!isFinite(ptsScale.domain()[0]) || !isFinite(ptsScale.domain()[1])) {
155 return;
156 }
157 var ptsAxis = d3.svg.axis().scale(ptsScale).orient('left');
158 svg.select('.axis.presentation')
159 .transition().duration(500)
160 .call(ptsAxis);
161 };
162 var svgRenderSegmentTimeline = function(container, player) {
163 var media = player.hls.playlists.media();
164 var segments = media.segments; // media.segments.slice(0, count);
165
166 // setup the display
167 var svg = d3.select(container)
168 .append('svg')
169 .attr('width', width + margin.left + margin.right)
170 .attr('height', height + margin.top + margin.bottom)
171 .append('g')
172 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
173
174 // setup the scales
175 var mediaScale = d3.scale.linear().range([0, width]);
176 mediaScale.domain(mediaDomain(media, player));
177 var ptsScale = d3.scale.linear().range([height, 0]);
178 ptsScale.domain(ptsDomain(segments, mediaScale));
179
180 // render
181 var mediaAxis = d3.svg.axis().scale(mediaScale).orient('bottom');
182 svg.append('g')
183 .attr('class', 'x axis media')
184 .attr('transform', 'translate(0,' + height + ')')
185 .call(mediaAxis);
186 var ptsAxis = d3.svg.axis().scale(ptsScale).orient('left');
187 svg.append('g')
188 .attr('class', 'y axis presentation')
189 .call(ptsAxis);
190
191 svg.append('path')
192 .attr('class', 'intersect')
193 .attr('d', 'M0,' + height + 'L' + width +',0');
194
195 var mediaOffset = 0;
196
197 // update everything on progress
198 player.on('progress', function() {
199 var updatedMedia = player.hls.playlists.media();
200 var segments = updatedMedia.segments; // updatedMedia.segments.slice(currentIndex, currentIndex + count);
201
202 if (updatedMedia.mediaSequence !== media.mediaSequence) {
203 mediaOffset += Playlist.duration(media,
204 media.mediaSequence,
205 updatedMedia.mediaSequence);
206 media = updatedMedia;
207 }
208
209 mediaScale.domain(mediaDomain(updatedMedia, player));
210 ptsScale.domain(ptsDomain(segments, mediaScale, mediaOffset));
211 svgUpdateAxes(svg, mediaScale, ptsScale, updatedMedia, segments);
212 if (!isFinite(ptsScale.domain()[0]) || !isFinite(ptsScale.domain()[1])) {
213 return;
214 }
215 for (var i = 0; i < player.textTracks().length; i++) {
216 var track = player.textTracks()[i];
217 svgUpdateCues(svg, mediaScale, ptsScale, ptsScale, track.cues);
218 }
219 });
220 };
221
222 var displayCues = function(container, player) {
223 var media = player.hls.playlists.media();
224 if (media && media.segments) {
225 svgRenderSegmentTimeline(container, player);
226 } else {
227 player.one('loadedmetadata', function() {
228 svgRenderSegmentTimeline(container, player);
229 });
230 }
231 };
232
233
234 // export
235 videojs.Hls.displayStats = displayStats;
236 videojs.Hls.displayCues = displayCues;
237
238 })(window, window.videojs);