├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── package.json ├── public ├── scripts │ ├── adaptive-streaming-player.js │ ├── basic-player.js │ └── buffering-player.js ├── style │ ├── adaptive.css │ └── basic.css └── vidData │ ├── example.json │ ├── example.webm │ ├── example1080.json │ ├── example1080.webm │ ├── example180.json │ └── example180.webm ├── routes └── index.js └── views ├── adaptive-streaming-player.html ├── basic-player.html ├── buffering-player.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 WIREWAX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Media Source Tutorial 2 | To host locally npm install and then node app.js 3 | 4 | 5 | This repo contains the source files for a [blog series on WIREWAX.com](https://www.wirewax.com/blog/post/building-a-media-source-html5-player). -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var express = require('express'), 6 | routes = require('./routes'), 7 | http = require('http'), 8 | path = require('path'), 9 | request = require('request') 10 | 11 | // Start app 12 | var app = express(); 13 | 14 | 15 | 16 | app.configure(function () { 17 | app.set('views', __dirname + '/views'); 18 | app.use(express.favicon()); 19 | app.use(express.logger('dev')); 20 | app.use(express.bodyParser()); 21 | app.use(express.methodOverride()); 22 | app.use(app.router); 23 | app.use('/', express.static(path.join(__dirname, 'public'))); 24 | app.engine('html', require('ejs').renderFile); 25 | app.use(express.errorHandler()); 26 | }); 27 | 28 | var server = app.listen(3105); 29 | 30 | exports = module.exports = app; 31 | 32 | // Routes 33 | app.get('/', routes['index']); 34 | app.get('/basic', routes['basicPlayer']); 35 | app.get('/buffering', routes['bufferingPlayer']); 36 | app.get('/adaptive', routes['adaptiveStreamingPlayer']); 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediaSourceTutorial", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "ejs": "^2.5.5", 6 | "express": "3.11.0", 7 | "request": "~2.68.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/scripts/adaptive-streaming-player.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | 4 | var BasicPlayer = function () { 5 | var self = this; 6 | self.clusters = []; 7 | self.renditions = ["180", "1080"]; 8 | self.rendition = "1080" 9 | 10 | 11 | function Cluster(fileUrl, rendition, byteStart, byteEnd, isInitCluster, timeStart, timeEnd) { 12 | this.byteStart = byteStart; //byte range start inclusive 13 | this.byteEnd = byteEnd; //byte range end exclusive 14 | this.timeStart = timeStart ? timeStart : -1; //timecode start inclusive 15 | this.timeEnd = timeEnd ? timeEnd : -1; //exclusive 16 | this.requested = false; //cluster download has started 17 | this.isInitCluster = isInitCluster; //is an init cluster 18 | this.queued = false; //cluster has been downloaded and queued to be appended to source buffer 19 | this.buffered = false; //cluster has been added to source buffer 20 | this.data = null; //cluster data from vid file 21 | 22 | this.fileUrl = fileUrl; 23 | this.rendition = rendition; 24 | this.requestedTime = null; 25 | this.queuedTime = null; 26 | } 27 | 28 | Cluster.prototype.download = function (callback) { 29 | this.requested = true; 30 | this.requestedTime = new Date().getTime(); 31 | this._getClusterData(function () { 32 | self.flushBufferQueue(); 33 | if (callback) { 34 | callback(); 35 | } 36 | }) 37 | }; 38 | Cluster.prototype._makeCacheBuster = function () { 39 | var text = ""; 40 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 41 | for (var i = 0; i < 10; i++) 42 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 43 | return text; 44 | }; 45 | Cluster.prototype._getClusterData = function (callback, retryCount) { 46 | var xhr = new XMLHttpRequest(); 47 | 48 | var vidUrl = self.sourceFile + this.rendition + '.webm'; 49 | if (retryCount) { 50 | vidUrl += '?cacheBuster=' + this._makeCacheBuster(); 51 | } 52 | xhr.open('GET', vidUrl, true); 53 | xhr.responseType = 'arraybuffer'; 54 | xhr.timeout = 6000; 55 | xhr.setRequestHeader('Range', 'bytes=' + this.byteStart + '-' + 56 | this.byteEnd); 57 | xhr.send(); 58 | var cluster = this; 59 | xhr.onload = function (e) { 60 | if (xhr.status !== 206 && xhr.stats !== 304) { 61 | console.error("media: Unexpected status code " + xhr.status); 62 | return false; 63 | } 64 | cluster.data = new Uint8Array(xhr.response); 65 | cluster.queued = true; 66 | cluster.queuedTime = new Date().getTime(); 67 | callback(); 68 | }; 69 | xhr.ontimeout = function () { 70 | var retryAmount = !retryCount ? 0 : retryCount; 71 | if (retryCount == 2) { 72 | console.error("Given up downloading") 73 | } else { 74 | cluster._getClusterData(callback, retryCount++); 75 | } 76 | } 77 | }; 78 | this.clearUp = function () { 79 | if (self.videoElement) { 80 | //clear down any resources from the previous video embed if it exists 81 | $(self.videoElement).remove(); 82 | delete self.mediaSource; 83 | delete self.sourceBuffer; 84 | self.clusters = []; 85 | self.rendition = "1080"; 86 | self.networkSpeed = null; 87 | $('#factor-display').html("0.0000"); 88 | $('#180-end').html("0.0"); 89 | $('#180-start').html("0.0"); 90 | $('#1080-end').html("0.0"); 91 | $('#1080-start').html("0.0"); 92 | $('#rendition').val("1080"); 93 | } 94 | } 95 | 96 | this.initiate = function (sourceFile, clusterFile) { 97 | if (!window.MediaSource || !MediaSource.isTypeSupported('video/webm; codecs="vp8,vorbis"')) { 98 | self.setState("Your browser is not supported"); 99 | return; 100 | } 101 | self.clearUp(); 102 | self.sourceFile = sourceFile; 103 | self.clusterFile = clusterFile; 104 | self.setState("Downloading cluster file"); 105 | self.downloadClusterData(function () { 106 | self.setState("Creating media source"); 107 | //create the video element 108 | self.videoElement = $('')[0]; 109 | //create the media source 110 | self.mediaSource = new MediaSource(); 111 | self.mediaSource.addEventListener('sourceopen', function () { 112 | self.setState("Creating source buffer"); 113 | //when the media source is opened create the source buffer 114 | self.createSourceBuffer(); 115 | }, false); 116 | //append the video element to the DOM 117 | self.videoElement.src = window.URL.createObjectURL(self.mediaSource); 118 | $('#basic-player').append($(self.videoElement)); 119 | }); 120 | } 121 | this.downloadClusterData = function (callback) { 122 | var totalRenditions = self.renditions.length; 123 | var renditionsDone = 0; 124 | _.each(self.renditions, function (rendition) { 125 | var xhr = new XMLHttpRequest(); 126 | 127 | var url = self.clusterFile + rendition + '.json'; 128 | xhr.open('GET', url, true); 129 | xhr.responseType = 'json'; 130 | 131 | xhr.send(); 132 | xhr.onload = function (e) { 133 | self.createClusters(xhr.response, rendition); 134 | renditionsDone++; 135 | if (renditionsDone === totalRenditions) { 136 | callback(); 137 | } 138 | }; 139 | }) 140 | } 141 | this.createClusters = function (rslt, rendition) { 142 | self.clusters.push(new Cluster( 143 | self.sourceFile + rendition + '.webm', 144 | rendition, 145 | rslt.init.offset, 146 | rslt.init.size - 1, 147 | true 148 | )); 149 | 150 | for (var i = 0; i < rslt.media.length; i++) { 151 | self.clusters.push(new Cluster( 152 | self.sourceFile + rendition + '.webm', 153 | rendition, 154 | rslt.media[i].offset, 155 | rslt.media[i].offset + rslt.media[i].size - 1, 156 | false, 157 | rslt.media[i].timecode, 158 | (i === rslt.media.length - 1) ? parseFloat(rslt.duration / 1000) : rslt.media[i + 1].timecode 159 | )); 160 | } 161 | } 162 | this.createSourceBuffer = function () { 163 | self.sourceBuffer = self.mediaSource.addSourceBuffer('video/webm; codecs="vp8,vorbis"'); 164 | self.sourceBuffer.addEventListener('updateend', function () { 165 | self.flushBufferQueue(); 166 | }, false); 167 | self.setState("Downloading clusters"); 168 | self.downloadInitCluster(self.downloadCurrentCluster); 169 | self.videoElement.addEventListener('timeupdate', function () { 170 | self.downloadUpcomingClusters(); 171 | self.checkBufferingSpeed(); 172 | }, false); 173 | } 174 | this.flushBufferQueue = function () { 175 | if (!self.sourceBuffer.updating) { 176 | var initCluster = _.findWhere(self.clusters, {isInitCluster: true, rendition: self.rendition}); 177 | if (initCluster.queued || initCluster.buffered) { 178 | var bufferQueue = _.filter(self.clusters, function (cluster) { 179 | return (cluster.queued === true && cluster.isInitCluster === false && cluster.rendition === self.rendition) 180 | }); 181 | if (!initCluster.buffered) { 182 | bufferQueue.unshift(initCluster); 183 | } 184 | if (bufferQueue.length) { 185 | var concatData = self.concatClusterData(bufferQueue); 186 | _.each(bufferQueue, function (bufferedCluster) { 187 | bufferedCluster.queued = false; 188 | bufferedCluster.buffered = true; 189 | }); 190 | self.sourceBuffer.appendBuffer(concatData); 191 | } 192 | } 193 | } 194 | } 195 | this.downloadInitCluster = function (callback) { 196 | _.findWhere(self.clusters, {isInitCluster: true, rendition: self.rendition}).download(callback); 197 | } 198 | this.downloadCurrentCluster = function () { 199 | var currentClusters = _.filter(self.clusters, function (cluster) { 200 | return (cluster.rendition === self.rendition && cluster.timeStart <= self.videoElement.currentTime && cluster.timeEnd > self.videoElement.currentTime) 201 | }); 202 | if (currentClusters.length === 1) { 203 | currentClusters[0].download(function () { 204 | self.setState("Downloaded current cluster"); 205 | }); 206 | } else { 207 | console.err("Something went wrong with download current cluster"); 208 | } 209 | } 210 | this.downloadUpcomingClusters = function () { 211 | var nextClusters = _.filter(self.clusters, function (cluster) { 212 | return (cluster.requested === false && cluster.rendition === self.rendition && cluster.timeStart > self.videoElement.currentTime && cluster.timeStart <= self.videoElement.currentTime + 5) 213 | }); 214 | if (nextClusters.length) { 215 | self.setState("Buffering ahead"); 216 | _.each(nextClusters, function (nextCluster) { 217 | nextCluster.download(); 218 | }); 219 | } else { 220 | if (_.filter(self.clusters, function (cluster) { 221 | return (cluster.requested === false ) 222 | }).length === 0) { 223 | self.setState("Finished buffering whole video"); 224 | } else { 225 | self.finished = true; 226 | self.setState("Finished buffering ahead"); 227 | } 228 | } 229 | } 230 | this.switchRendition = function (rendition) { 231 | self.rendition = rendition; 232 | self.downloadInitCluster(); 233 | self.downloadUpcomingClusters(); 234 | $('#rendition').val(rendition); 235 | } 236 | this.concatClusterData = function (clusterList) { 237 | var bufferArrayList = []; 238 | _.each(clusterList, function (cluster) { 239 | bufferArrayList.push(cluster.data); 240 | }) 241 | var arrLength = 0; 242 | _.each(bufferArrayList, function (bufferArray) { 243 | arrLength += bufferArray.length; 244 | }); 245 | var returnArray = new Uint8Array(arrLength); 246 | var lengthSoFar = 0; 247 | _.each(bufferArrayList, function (bufferArray, idx) { 248 | returnArray.set(bufferArray, lengthSoFar); 249 | lengthSoFar += bufferArray.length 250 | }); 251 | return returnArray; 252 | }; 253 | 254 | this.setState = function (state) { 255 | $('#state-display').html(state); 256 | } 257 | 258 | 259 | this.downloadTimeMR = _.memoize( 260 | function (downloadedClusters) { // map reduce function to get download time per byte 261 | return _.chain(downloadedClusters 262 | .map(function (cluster) { 263 | return { 264 | size: cluster.byteEnd - cluster.byteStart, 265 | time: cluster.queuedTime - cluster.requestedTime 266 | }; 267 | }) 268 | .reduce(function (memo, datum) { 269 | return { 270 | size: memo.size + datum.size, 271 | time: memo.time + datum.time 272 | } 273 | }, {size: 0, time: 0}) 274 | ).value() 275 | }, function (downloadedClusters) { 276 | return downloadedClusters.length; //hash function is the length of the downloaded clusters as it should be strictly increasing 277 | } 278 | ); 279 | this.getClustersSorted = function (rendition) { 280 | return _.chain(self.clusters) 281 | .filter(function (cluster) { 282 | return (cluster.buffered === true && cluster.rendition == rendition && cluster.isInitCluster === false); 283 | }) 284 | .sortBy(function (cluster) { 285 | return cluster.byteStart 286 | }) 287 | .value(); 288 | } 289 | this.getNextCluster = function () { 290 | var unRequestedUpcomingClusters = _.chain(self.clusters) 291 | .filter(function (cluster) { 292 | return (!cluster.requested && cluster.timeStart >= self.videoElement.currentTime && cluster.rendition === self.rendition); 293 | }) 294 | .sortBy(function (cluster) { 295 | return cluster.byteStart 296 | }) 297 | .value(); 298 | if (unRequestedUpcomingClusters.length) { 299 | return unRequestedUpcomingClusters[0]; 300 | } else { 301 | self.setState('Completed video buffering') 302 | throw new Error("No more upcoming clusters"); 303 | } 304 | }; 305 | 306 | 307 | this.getDownloadTimePerByte = function () { //seconds per byte 308 | var mapOut = this.downloadTimeMR(_.filter(self.clusters, function (cluster) { 309 | return (cluster.queued || cluster.buffered) 310 | })); 311 | var res = ((mapOut.time / 1000) / mapOut.size); 312 | return res; 313 | }; 314 | this.checkBufferingSpeed = function () { 315 | var secondsToDownloadPerByte = self.getDownloadTimePerByte(); 316 | var nextCluster = self.getNextCluster(); 317 | var upcomingBytesPerSecond = (nextCluster.byteEnd - nextCluster.byteStart) / (nextCluster.timeEnd - nextCluster.timeStart); 318 | var estimatedSecondsToDownloadPerSecondOfPlayback = secondsToDownloadPerByte * upcomingBytesPerSecond; 319 | 320 | var overridenFactor = self.networkSpeed ? self.networkSpeed : Math.round(estimatedSecondsToDownloadPerSecondOfPlayback * 10000) / 10000; 321 | 322 | $('#factor-display').html(overridenFactor); 323 | 324 | var lowClusters = this.getClustersSorted("180"); 325 | if (lowClusters.length) { 326 | $('#180-end').html(Math.round(lowClusters[lowClusters.length - 1].timeEnd*10)/10); 327 | $('#180-start').html(lowClusters[0].timeStart === -1 ? "0.0" :Math.round(lowClusters[0].timeStart*10)/10); 328 | } 329 | 330 | var highClusters = this.getClustersSorted("1080"); 331 | if (highClusters.length) { 332 | $('#1080-end').html(Math.round(highClusters[highClusters.length - 1].timeEnd*10)/10); 333 | $('#1080-start').html(highClusters[0].timeStart === -1 ? "0.0" : Math.round(highClusters[0].timeStart*10)/10); 334 | } 335 | 336 | if (overridenFactor > 0.8) { 337 | if (self.rendition !== "180") { 338 | self.switchRendition("180") 339 | } 340 | } else { 341 | //do this if you want to move rendition up automatically 342 | //if (self.rendition !== "1080") { 343 | // self.switchRendition("1080") 344 | //} 345 | } 346 | } 347 | 348 | 349 | } 350 | 351 | var basicPlayer = new BasicPlayer(); 352 | window.updatePlayer = function () { 353 | var sourceFile = 'vidData/example'; 354 | var clusterData = 'vidData/example'; 355 | basicPlayer.initiate(sourceFile, clusterData); 356 | } 357 | updatePlayer(); 358 | $('#rendition').change(function () { 359 | basicPlayer.switchRendition($('#rendition').val()); 360 | }); 361 | $('#simulate-button').click(function () { 362 | basicPlayer.networkSpeed = 2; 363 | $('#factor-display').html(2); 364 | $('#simulate-button').addClass('ww4-active'); 365 | }) 366 | $('#restart').click(function() { 367 | $('#simulate-button').removeClass('ww4-active'); 368 | updatePlayer(); 369 | }); 370 | 371 | }); -------------------------------------------------------------------------------- /public/scripts/basic-player.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var BasicPlayer = function () { 4 | var self = this; 5 | this.clearUp = function() { 6 | if (self.videoElement) { 7 | //clear down any resources from the previous video embed if it exists 8 | $(self.videoElement).remove(); 9 | delete self.mediaSource; 10 | delete self.sourceBuffer; 11 | } 12 | } 13 | this.initiate = function (sourceFile) { 14 | if (!window.MediaSource || !MediaSource.isTypeSupported('video/webm; codecs="vp8,vorbis"')) { 15 | self.setState("Your browser is not supported"); 16 | return; 17 | } 18 | self.clearUp(); 19 | self.sourceFile = sourceFile; 20 | self.setState("Creating media source using"); 21 | //create the video element 22 | self.videoElement = $('')[0]; 23 | //create the media source 24 | self.mediaSource = new MediaSource(); 25 | self.mediaSource.addEventListener('sourceopen', function () { 26 | self.setState("Creating source buffer"); 27 | //when the media source is opened create the source buffer 28 | self.createSourceBuffer(); 29 | }, false); 30 | //append the video element to the DOM 31 | self.videoElement.src = window.URL.createObjectURL(self.mediaSource); 32 | $('#basic-player').append($(self.videoElement)); 33 | } 34 | this.createSourceBuffer = function () { 35 | 36 | self.sourceBuffer = self.mediaSource.addSourceBuffer('video/webm; codecs="vp8,vorbis"'); 37 | self.sourceBuffer.addEventListener('updateend', function () { 38 | self.setState("Ready"); 39 | }, false); 40 | var xhr = new XMLHttpRequest(); 41 | xhr.open('GET', self.sourceFile, true); 42 | xhr.responseType = 'arraybuffer'; 43 | xhr.onload = function (e) { 44 | if (xhr.status !== 200) { 45 | self.setState("Failed to download video data"); 46 | self.clearUp(); 47 | } else { 48 | var arr = new Uint8Array(xhr.response); 49 | if (!self.sourceBuffer.updating) { 50 | self.setState("Appending video data to buffer"); 51 | self.sourceBuffer.appendBuffer(arr); 52 | } else { 53 | self.setState("Source Buffer failed to update"); 54 | } 55 | } 56 | }; 57 | xhr.onerror = function () { 58 | self.setState("Failed to download video data"); 59 | self.clearUp(); 60 | }; 61 | xhr.send(); 62 | self.setState("Downloading video data"); 63 | } 64 | this.setState = function (state) { 65 | $('#state-display').html(state); 66 | } 67 | } 68 | 69 | var basicPlayer = new BasicPlayer(); 70 | 71 | window.updatePlayer = function () { 72 | var sourceFile = $('#source-file').val(); 73 | basicPlayer.initiate(sourceFile); 74 | } 75 | updatePlayer(); 76 | $('#embed').click(updatePlayer); 77 | }); -------------------------------------------------------------------------------- /public/scripts/buffering-player.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | 4 | var BasicPlayer = function () { 5 | var self = this; 6 | self.clusters = []; 7 | 8 | 9 | function Cluster(byteStart, byteEnd, isInitCluster, timeStart, timeEnd) { 10 | this.byteStart = byteStart; //byte range start inclusive 11 | this.byteEnd = byteEnd; //byte range end exclusive 12 | this.timeStart = timeStart ? timeStart : -1; //timecode start inclusive 13 | this.timeEnd = timeEnd ? timeEnd : -1; //exclusive 14 | this.requested = false; //cluster download has started 15 | this.isInitCluster = isInitCluster; //is an init cluster 16 | this.queued = false; //cluster has been downloaded and queued to be appended to source buffer 17 | this.buffered = false; //cluster has been added to source buffer 18 | this.data = null; //cluster data from vid file 19 | } 20 | 21 | Cluster.prototype.download = function (callback) { 22 | this.requested = true; 23 | this._getClusterData(function () { 24 | self.flushBufferQueue(); 25 | if (callback) { 26 | callback(); 27 | } 28 | }) 29 | }; 30 | Cluster.prototype._makeCacheBuster = function () { 31 | var text = ""; 32 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 33 | for (var i = 0; i < 10; i++) 34 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 35 | return text; 36 | }; 37 | Cluster.prototype._getClusterData = function (callback, retryCount) { 38 | var xhr = new XMLHttpRequest(); 39 | 40 | var vidUrl = self.sourceFile; 41 | if (retryCount) { 42 | vidUrl += '?cacheBuster=' + this._makeCacheBuster(); 43 | } 44 | xhr.open('GET', vidUrl, true); 45 | xhr.responseType = 'arraybuffer'; 46 | xhr.timeout = 6000; 47 | xhr.setRequestHeader('Range', 'bytes=' + this.byteStart + '-' + 48 | this.byteEnd); 49 | xhr.send(); 50 | var cluster = this; 51 | xhr.onload = function (e) { 52 | if (xhr.status != 206) { 53 | console.err("media: Unexpected status code " + xhr.status); 54 | return false; 55 | } 56 | cluster.data = new Uint8Array(xhr.response); 57 | ; 58 | cluster.queued = true; 59 | callback(); 60 | }; 61 | xhr.ontimeout = function () { 62 | var retryAmount = !retryCount ? 0 : retryCount; 63 | if (retryCount == 2) { 64 | console.err("Given up downloading") 65 | } else { 66 | cluster._getClusterData(callback, retryCount++); 67 | } 68 | } 69 | }; 70 | 71 | 72 | this.clearUp = function () { 73 | if (self.videoElement) { 74 | //clear down any resources from the previous video embed if it exists 75 | $(self.videoElement).remove(); 76 | delete self.mediaSource; 77 | delete self.sourceBuffer; 78 | } 79 | } 80 | this.initiate = function (sourceFile, clusterFile) { 81 | if (!window.MediaSource || !MediaSource.isTypeSupported('video/webm; codecs="vp8,vorbis"')) { 82 | self.setState("Your browser is not supported"); 83 | return; 84 | } 85 | 86 | self.clearUp(); 87 | self.sourceFile = sourceFile; 88 | self.clusterFile = clusterFile; 89 | self.setState("Downloading cluster file"); 90 | self.downloadClusterData(function () { 91 | self.setState("Creating media source"); 92 | //create the video element 93 | self.videoElement = $('')[0]; 94 | //create the media source 95 | self.mediaSource = new MediaSource(); 96 | self.mediaSource.addEventListener('sourceopen', function () { 97 | self.setState("Creating source buffer"); 98 | //when the media source is opened create the source buffer 99 | self.createSourceBuffer(); 100 | }, false); 101 | //append the video element to the DOM 102 | self.videoElement.src = window.URL.createObjectURL(self.mediaSource); 103 | $('#basic-player').append($(self.videoElement)); 104 | }); 105 | } 106 | this.downloadClusterData = function (callback) { 107 | var xhr = new XMLHttpRequest(); 108 | 109 | var url = self.clusterFile; 110 | xhr.open('GET', url, true); 111 | xhr.responseType = 'json'; 112 | 113 | xhr.send(); 114 | xhr.onload = function (e) { 115 | self.createClusters(xhr.response); 116 | console.log("clusters", self.clusters); 117 | callback(); 118 | }; 119 | } 120 | this.createClusters = function (rslt) { 121 | self.clusters.push(new Cluster( 122 | rslt.init.offset, 123 | rslt.init.size - 1, 124 | true 125 | )); 126 | 127 | for (var i = 0; i < rslt.media.length; i++) { 128 | self.clusters.push(new Cluster( 129 | rslt.media[i].offset, 130 | rslt.media[i].offset + rslt.media[i].size - 1, 131 | false, 132 | rslt.media[i].timecode, 133 | (i === rslt.media.length - 1) ? parseFloat(rslt.duration / 1000) : rslt.media[i + 1].timecode 134 | )); 135 | } 136 | } 137 | this.createSourceBuffer = function () { 138 | self.sourceBuffer = self.mediaSource.addSourceBuffer('video/webm; codecs="vp8,vorbis"'); 139 | self.sourceBuffer.addEventListener('updateend', function () { 140 | self.flushBufferQueue(); 141 | }, false); 142 | self.setState("Downloading clusters"); 143 | self.downloadInitCluster(); 144 | self.videoElement.addEventListener('timeupdate',function(){ 145 | self.downloadUpcomingClusters(); 146 | },false); 147 | } 148 | this.flushBufferQueue = function () { 149 | if (!self.sourceBuffer.updating) { 150 | var initCluster = _.findWhere(self.clusters, {isInitCluster: true}); 151 | if (initCluster.queued || initCluster.buffered) { 152 | var bufferQueue = _.filter(self.clusters, function (cluster) { 153 | return (cluster.queued === true && cluster.isInitCluster === false) 154 | }); 155 | if (!initCluster.buffered) { 156 | bufferQueue.unshift(initCluster); 157 | } 158 | if (bufferQueue.length) { 159 | var concatData = self.concatClusterData(bufferQueue); 160 | _.each(bufferQueue, function (bufferedCluster) { 161 | bufferedCluster.queued = false; 162 | bufferedCluster.buffered = true; 163 | }); 164 | self.sourceBuffer.appendBuffer(concatData); 165 | } 166 | } 167 | } 168 | } 169 | this.downloadInitCluster = function () { 170 | _.findWhere(self.clusters, {isInitCluster: true}).download(self.downloadUpcomingClusters); 171 | } 172 | this.downloadUpcomingClusters = function () { 173 | var nextClusters = _.filter(self.clusters, function (cluster) { 174 | return (cluster.requested === false && cluster.timeStart <= self.videoElement.currentTime + 5) 175 | }); 176 | if (nextClusters.length) { 177 | _.each(nextClusters, function (nextCluster) { 178 | nextCluster.download(); 179 | }); 180 | } else { 181 | if (_.filter(self.clusters, function (cluster) { 182 | return (cluster.requested === false ) 183 | }).length === 0) { 184 | self.setState("Finished buffering whole video"); 185 | } else { 186 | self.finished = true; 187 | self.setState("Finished buffering ahead"); 188 | } 189 | } 190 | } 191 | this.concatClusterData = function (clusterList) { 192 | console.log(clusterList); 193 | var bufferArrayList = []; 194 | _.each(clusterList, function (cluster) { 195 | bufferArrayList.push(cluster.data); 196 | }) 197 | var arrLength = 0; 198 | _.each(bufferArrayList, function (bufferArray) { 199 | arrLength += bufferArray.length; 200 | }); 201 | var returnArray = new Uint8Array(arrLength); 202 | var lengthSoFar = 0; 203 | _.each(bufferArrayList, function (bufferArray, idx) { 204 | returnArray.set(bufferArray, lengthSoFar); 205 | lengthSoFar += bufferArray.length 206 | }); 207 | return returnArray; 208 | }; 209 | 210 | this.setState = function (state) { 211 | $('#state-display').html(state); 212 | } 213 | } 214 | 215 | var basicPlayer = new BasicPlayer(); 216 | 217 | window.updatePlayer = function () { 218 | var sourceFile = 'vidData/example.webm'; 219 | var clusterData = 'vidData/example.json'; 220 | basicPlayer.initiate(sourceFile, clusterData); 221 | } 222 | updatePlayer(); 223 | }); -------------------------------------------------------------------------------- /public/style/adaptive.css: -------------------------------------------------------------------------------- 1 | #player-embed { 2 | width: 100%; 3 | height: 0px; 4 | position: relative; 5 | padding-bottom: 56.25%; 6 | margin-bottom: 10px; 7 | } 8 | 9 | #basic-player { 10 | height: 100%; 11 | width: 100%; 12 | border: 1px solid #000000; 13 | position: absolute; 14 | } 15 | 16 | .state { 17 | margin: 10px; 18 | } 19 | video { 20 | height: 100%; 21 | width: 100%; 22 | position: absolute; 23 | left: 0px; 24 | top: 0px; 25 | } 26 | 27 | table.ww4-table { 28 | margin:auto; 29 | margin-top: 10px; 30 | font-family: futura-tee, verdana, arial; 31 | border-collapse: collapse; 32 | color: #989898; 33 | padding: 0px; 34 | } 35 | table.ww4-table tr { 36 | border: 1px solid #f5f5f5; 37 | } 38 | table.ww4-table th { 39 | padding: 10px; 40 | font-weight: normal; 41 | text-align: left; 42 | } 43 | table.ww4-table td { 44 | padding: 10px; 45 | text-align: left; 46 | } 47 | .icon { 48 | width: 30px; 49 | height: 30px; 50 | stroke: #989898; 51 | fill: #989898; 52 | 53 | } 54 | .icon svg path { 55 | stroke-width: 1.3; 56 | vector-effect: non-scaling-stroke; 57 | } 58 | .ww4-select { 59 | border-radius: 5px; 60 | height: 40px; 61 | display: inline-block; 62 | border: 1px solid #e5e5e5; 63 | background-color: white; 64 | cursor: pointer; 65 | -webkit-transition: all linear 0.3s; 66 | transition: all linear 0.3s; 67 | position: relative; 68 | vertical-align: middle; 69 | overflow: hidden; 70 | min-width: 100px; 71 | padding-left: 25px; 72 | color: #7f7f7f; 73 | min-width: 140px; 74 | outline: 0; 75 | } 76 | 77 | .ww4-static-button { 78 | border-radius: 20px; 79 | height: 40px; 80 | display: inline-block; 81 | border: 1px solid #7bafa8; 82 | background-color: white; 83 | cursor: pointer; 84 | -webkit-transition: all linear 0.3s; 85 | transition: all linear 0.3s; 86 | position: relative; 87 | vertical-align: middle; 88 | overflow: hidden; 89 | } 90 | .ww4-static-button.ww4-active { 91 | background-color: #7bafa8; 92 | border-color: #7bafa8; 93 | } 94 | .ww4-static-button.ww4-active p { 95 | color: #f5f5f5; 96 | } 97 | .ww4-static-button .static-button-icon-container { 98 | width: 38px; 99 | height: 100%; 100 | position: absolute; 101 | } 102 | .ww4-static-button .static-button-icon { 103 | width: 22px; 104 | height: 22px; 105 | margin: 8px auto auto; 106 | } 107 | .ww4-static-button .static-button-icon svg { 108 | width: 100%; 109 | height: 100%; 110 | 111 | } 112 | .ww4-static-button .static-button-icon svg path { 113 | stroke-width: 1.2; 114 | vector-effect: non-scaling-stroke; 115 | } 116 | .ww4-active .static-button-icon svg { 117 | stroke: #f5f5f5 !important; 118 | fill: #f5f5f5 !important; 119 | } 120 | .ww4-static-button .static-button-icon svg { 121 | -webkit-transition: all linear 0.3s; 122 | transition: all linear 0.3s; 123 | stroke: #989898; 124 | fill: #989898; 125 | } 126 | .ww4-static-button p { 127 | font-size: 14px; 128 | position: relative; 129 | top: 50%; 130 | -webkit-transform: translateY(-50%); 131 | transform: translateY(-50%); 132 | margin: 0 20px 0 20px; 133 | color: #989898; 134 | -webkit-transition: all linear 0.3s; 135 | transition: all linear 0.3s; 136 | text-align: center; 137 | margin-left: 40px; 138 | } 139 | .ww4-static-button:hover:not(.ww4-disabled):not(.ww4-confirm-mode):not(.ww4-error):not(.ww4-active) { 140 | background-color: #7bafa8; 141 | border-color: #7bafa8; 142 | -webkit-transition: none; 143 | transition: none; 144 | } 145 | .ww4-static-button:hover:not(.ww4-disabled):not(.ww4-confirm-mode):not(.ww4-error):not(.ww4-active) p { 146 | color: #f5f5f5; 147 | -webkit-transition: none; 148 | transition: none; 149 | } 150 | .ww4-static-button:hover:not(.ww4-disabled):not(.ww4-confirm-mode):not(.ww4-error):not(.ww4-active) .static-button-icon svg { 151 | stroke: #f5f5f5; 152 | fill: #f5f5f5; 153 | -webkit-transition: none; 154 | transition: none; 155 | } -------------------------------------------------------------------------------- /public/style/basic.css: -------------------------------------------------------------------------------- 1 | #player-embed { 2 | width: 100%; 3 | height: 0px; 4 | position: relative; 5 | padding-bottom: 56.25%; 6 | margin-bottom: 10px; 7 | } 8 | 9 | #basic-player { 10 | height: 100%; 11 | width: 100%; 12 | border: 1px solid #000000; 13 | position: absolute; 14 | } 15 | 16 | .state { 17 | margin: 10px; 18 | } 19 | 20 | video { 21 | height: 100%; 22 | width: 100%; 23 | position: absolute; 24 | left: 0px; 25 | top: 0px; 26 | } 27 | 28 | table.ww4-table { 29 | margin:auto; 30 | margin-top: 10px; 31 | font-family: futura-tee, verdana, arial; 32 | border-collapse: collapse; 33 | color: #989898; 34 | padding: 0px; 35 | min-width: 300px; 36 | } 37 | table.ww4-table tr { 38 | border: 1px solid #f5f5f5; 39 | } 40 | table.ww4-table th { 41 | padding: 10px; 42 | font-weight: normal; 43 | text-align: left; 44 | } 45 | table.ww4-table td { 46 | padding: 10px; 47 | text-align: left; 48 | } 49 | .icon { 50 | width: 30px; 51 | height: 30px; 52 | stroke: #989898; 53 | fill: #989898; 54 | 55 | } 56 | .icon svg path { 57 | stroke-width: 1.3; 58 | vector-effect: non-scaling-stroke; 59 | } 60 | .ww4-input { 61 | height: 40px; 62 | display: inline-block; 63 | border: 1px solid #e5e5e5; 64 | border-radius: 5px; 65 | background-color: white; 66 | position: relative; 67 | vertical-align: middle; 68 | overflow: hidden; 69 | padding-left: 5px; 70 | color: #7f7f7f; 71 | width: 100%; 72 | } 73 | .ww4-select { 74 | border-radius: 20px; 75 | height: 40px; 76 | display: inline-block; 77 | border: 1px solid #7bafa8; 78 | background-color: white; 79 | cursor: pointer; 80 | -webkit-transition: all linear 0.3s; 81 | transition: all linear 0.3s; 82 | position: relative; 83 | vertical-align: middle; 84 | overflow: hidden; 85 | min-width: 100px; 86 | padding-left: 25px; 87 | color: #989898; 88 | min-width: 140px; 89 | } 90 | 91 | .ww4-static-button { 92 | border-radius: 20px; 93 | height: 40px; 94 | display: inline-block; 95 | border: 1px solid #7bafa8; 96 | background-color: white; 97 | cursor: pointer; 98 | -webkit-transition: all linear 0.3s; 99 | transition: all linear 0.3s; 100 | position: relative; 101 | vertical-align: middle; 102 | overflow: hidden; 103 | } 104 | .ww4-static-button.ww4-active { 105 | background-color: #7bafa8; 106 | border-color: #7bafa8; 107 | } 108 | .ww4-static-button.ww4-active p { 109 | color: #f5f5f5; 110 | } 111 | .ww4-static-button .static-button-icon-container { 112 | width: 38px; 113 | height: 100%; 114 | position: absolute; 115 | } 116 | .ww4-static-button .static-button-icon { 117 | width: 22px; 118 | height: 22px; 119 | margin: 10px auto auto; 120 | } 121 | .ww4-static-button .static-button-icon svg { 122 | width: 100%; 123 | height: 100%; 124 | 125 | } 126 | .ww4-static-button .static-button-icon svg path { 127 | stroke-width: 1.2; 128 | vector-effect: non-scaling-stroke; 129 | } 130 | .ww4-active .static-button-icon svg { 131 | stroke: #f5f5f5 !important; 132 | fill: #f5f5f5 !important; 133 | } 134 | .ww4-static-button .static-button-icon svg { 135 | -webkit-transition: all linear 0.3s; 136 | transition: all linear 0.3s; 137 | stroke: #989898; 138 | fill: #989898; 139 | } 140 | .ww4-static-button p { 141 | font-size: 14px; 142 | position: relative; 143 | top: 50%; 144 | -webkit-transform: translateY(-50%); 145 | transform: translateY(-50%); 146 | margin: 0 20px 0 20px; 147 | color: #989898; 148 | -webkit-transition: all linear 0.3s; 149 | transition: all linear 0.3s; 150 | text-align: center; 151 | margin-left: 40px; 152 | } 153 | .ww4-static-button:hover:not(.ww4-disabled):not(.ww4-confirm-mode):not(.ww4-error):not(.ww4-active) { 154 | background-color: #7bafa8; 155 | border-color: #7bafa8; 156 | -webkit-transition: none; 157 | transition: none; 158 | } 159 | .ww4-static-button:hover:not(.ww4-disabled):not(.ww4-confirm-mode):not(.ww4-error):not(.ww4-active) p { 160 | color: #f5f5f5; 161 | -webkit-transition: none; 162 | transition: none; 163 | } 164 | .ww4-static-button:hover:not(.ww4-disabled):not(.ww4-confirm-mode):not(.ww4-error):not(.ww4-active) .static-button-icon svg { 165 | stroke: #f5f5f5; 166 | fill: #f5f5f5; 167 | -webkit-transition: none; 168 | transition: none; 169 | } -------------------------------------------------------------------------------- /public/vidData/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "video/webm; codecs=\"vp8, vorbis\"", 3 | "duration": 30892.000000, 4 | "init": { "offset": 0, "size": 4651}, 5 | "media": [ 6 | { "offset": 4651, "size": 962246, "timecode": 0.000000 }, 7 | { "offset": 966897, "size": 660411, "timecode": 9.991000 }, 8 | { "offset": 1627308, "size": 721264, "timecode": 19.999000 }, 9 | { "offset": 2348572, "size": 83217, "timecode": 29.984000 } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /public/vidData/example.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wireWAX/media-source-tutorial/fe8b97ecb15108aa95ac3de2de4e315edb418a05/public/vidData/example.webm -------------------------------------------------------------------------------- /public/vidData/example1080.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "video/webm; codecs=\"vp8, vorbis\"", 3 | "duration": 30892.000000, 4 | "init": { "offset": 0, "size": 4651}, 5 | "media": [ 6 | { "offset": 4651, "size": 962246, "timecode": 0.000000 }, 7 | { "offset": 966897, "size": 660411, "timecode": 9.991000 }, 8 | { "offset": 1627308, "size": 721264, "timecode": 19.999000 }, 9 | { "offset": 2348572, "size": 83217, "timecode": 29.984000 } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /public/vidData/example1080.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wireWAX/media-source-tutorial/fe8b97ecb15108aa95ac3de2de4e315edb418a05/public/vidData/example1080.webm -------------------------------------------------------------------------------- /public/vidData/example180.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "video/webm; codecs=\"vp8, vorbis\"", 3 | "duration": 30892.000000, 4 | "init": { "offset": 0, "size": 4651}, 5 | "media": [ 6 | { "offset": 4651, "size": 962246, "timecode": 0.000000 }, 7 | { "offset": 966897, "size": 660411, "timecode": 9.991000 }, 8 | { "offset": 1627308, "size": 721264, "timecode": 19.999000 }, 9 | { "offset": 2348572, "size": 83217, "timecode": 29.984000 } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /public/vidData/example180.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wireWAX/media-source-tutorial/fe8b97ecb15108aa95ac3de2de4e315edb418a05/public/vidData/example180.webm -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | exports.index = function(req, res) { 2 | res.render('index.html'); 3 | }; 4 | exports.basicPlayer = function(req , res){ 5 | res.render('basic-player.html'); 6 | }; 7 | exports.bufferingPlayer = function(req , res){ 8 | res.render('buffering-player.html'); 9 | }; 10 | exports.adaptiveStreamingPlayer = function(req , res){ 11 | res.render('adaptive-streaming-player.html'); 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /views/adaptive-streaming-player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 44 | 45 | 51 | 52 | 53 | 66 | 67 | 68 | 69 | 70 | 71 | 81 | 82 | 83 | 84 | 85 | 86 | 96 | 97 | 98 | 99 | 100 | 101 | 117 | 134 | 135 |
15 |
16 | 18 | 21 | 22 | 23 |
24 | 25 |
Video StatusWaiting for Embed
34 |
35 | 37 | 39 | 40 | 41 |
42 | 43 |
Downloading Rendition 46 | 50 |
54 |
55 | 57 | 62 | 63 |
64 | 65 |
Download Time Ratio0.0000
72 |
73 | 75 | 77 | 78 | 79 |
80 |
180 Buffered Data0.0-0.0s
87 |
88 | 90 | 92 | 93 | 94 |
95 |
1080 Buffered Data0.0-0.0s
102 |
103 |
104 |
105 | 107 | 110 | 111 | 112 | 113 |
114 |
115 |

Simulate Network Slowdown

116 |
118 |
119 |
120 |
121 | 122 | 123 | 125 | 128 | 129 | 130 |
131 |
132 |

Restart Player

133 |
136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /views/basic-player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | 13 | 16 | 19 | 20 | 21 | 24 | 25 | 36 | 37 |
14 | Video State 15 | 17 | Waiting for Embed 18 |
22 | 23 |
26 |
27 |
28 |
29 | Created with Snap 30 | 31 | 32 |
33 |
34 |

Embed Video

35 |
38 | 39 | -------------------------------------------------------------------------------- /views/buffering-player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
15 | Video State 16 | 18 | Waiting for Embed 19 |
8 | 9 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 |
10 | Basic player 11 |
15 | Buffering player 16 |
20 | Adaptive streaming player 21 |
25 | 26 | --------------------------------------------------------------------------------