├── .gitignore
├── public
├── vidData
│ ├── example.webm
│ ├── example1080.webm
│ ├── example180.webm
│ ├── example.json
│ ├── example1080.json
│ └── example180.json
├── scripts
│ ├── basic-player.js
│ ├── buffering-player.js
│ └── adaptive-streaming-player.js
└── style
│ ├── adaptive.css
│ └── basic.css
├── package.json
├── README.md
├── routes
└── index.js
├── views
├── index.html
├── buffering-player.html
├── basic-player.html
└── adaptive-streaming-player.html
├── app.js
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/public/vidData/example.webm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wireWAX/media-source-tutorial/HEAD/public/vidData/example.webm
--------------------------------------------------------------------------------
/public/vidData/example1080.webm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wireWAX/media-source-tutorial/HEAD/public/vidData/example1080.webm
--------------------------------------------------------------------------------
/public/vidData/example180.webm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wireWAX/media-source-tutorial/HEAD/public/vidData/example180.webm
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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).
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
25 |
26 |
--------------------------------------------------------------------------------
/views/buffering-player.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 | Video State
16 |
17 |
18 | Waiting for Embed
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/views/basic-player.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
38 |
39 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/views/adaptive-streaming-player.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
24 |
25 |
26 | Video Status
27 | Waiting for Embed
28 |
29 |
30 |
31 |
32 |
33 |
34 |
42 |
43 |
44 | Downloading Rendition
45 |
46 |
47 | 1080
48 | 180
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
66 | Download Time Ratio
67 |
68 | 0.0000
69 |
70 |
71 |
72 |
80 |
81 | 180 Buffered Data
82 |
83 | 0.0 -0.0 s
84 |
85 |
86 |
87 |
95 |
96 | 1080 Buffered Data
97 |
98 | 0.0 -0.0 s
99 |
100 |
101 |
102 |
116 |
117 |
118 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/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/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 | });
--------------------------------------------------------------------------------