├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gulpfile.js ├── LICENSE ├── README.md ├── bin ├── .hook_template └── hook.js ├── index.html ├── index.js ├── karma.conf.js ├── package.json ├── public ├── .gitkeep ├── P2PHLSPlayer.swf ├── p2phls_playback.html └── style.css ├── src ├── adaptive_streaming.js ├── cdn_requester.js ├── flash_uploader.js ├── jst.js ├── log.js ├── main.js ├── md5.js ├── p2p_manager.js ├── peer.js ├── playback_info.js ├── resource_requester.js ├── settings.js ├── storage.js ├── styler.js ├── swarm.js ├── swarm_utils.js └── upload_handler.js └── test ├── flash_uploader_spec.js └── storage_spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | aws.json 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailing": true, 3 | "node": true, 4 | "quotmark": false, 5 | "asi": true, 6 | "expr": true, 7 | "esnext": true, 8 | "eqeqeq": true, 9 | "noempty": true, 10 | "unused": true, 11 | "unused": true, 12 | "trailing": true, 13 | "smarttabs": true, 14 | "white": true 15 | } 16 | 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - "sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test" 6 | - "sudo apt-get update -qq" 7 | - "export DISPLAY=:99.0" 8 | - "sh -e /etc/init.d/xvfb start" 9 | 10 | install: 11 | - "sudo apt-get install -qq gcc-4.8 g++-4.8" 12 | - "sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 90" 13 | - "npm install" 14 | 15 | env: 16 | - CHROME_BIN=/usr/bin/chromium-browser 17 | 18 | script: npm test 19 | 20 | notifications: 21 | email: 22 | - flavio@bem.tv 23 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sass = require('gulp-sass'); 3 | var minifyCSS = require('gulp-minify-css'); 4 | var es6ify = require('es6ify'); 5 | var rename = require('gulp-rename'); 6 | var browserify = require('browserify'); 7 | var uglify = require('gulp-uglify'); 8 | var streamify = require('gulp-streamify') 9 | var source = require('vinyl-source-stream'); 10 | var exec = require('child_process').exec; 11 | var args = require('yargs').argv; 12 | var express = require('express'); 13 | var util = require('gulp-util'); 14 | var livereload = require('gulp-livereload'); 15 | var s3 = require('s3'); 16 | var fs = require('fs'); 17 | 18 | var files = { 19 | css: 'public/*.css', 20 | scss: 'public/*.scss', 21 | html: 'public/*.html' 22 | }; 23 | 24 | gulp.task('pre-build', ['sass', 'copy-html', 'copy-css'], function(done) { 25 | exec('node bin/hook.js', done); 26 | }); 27 | 28 | gulp.task('build', ['pre-build'], function(b) { 29 | var isProd = ['prod', 'production'].indexOf(args.env) !== -1 ? true : false; 30 | 31 | var stream = browserify() 32 | .transform(es6ify.configure(/^(?!.*node_modules)+.+\.js$/)) 33 | .add(es6ify.runtime) 34 | .add('./index.js', {entry: true}) 35 | .external('base_object') 36 | .external('playback') 37 | .external('browser') 38 | .external('zepto') 39 | .external('underscore') 40 | .external('hls') 41 | .bundle() 42 | .pipe(source('main.js')) 43 | .pipe(rename('p2phls' + (isProd ? '.min.js' : '.js'))); 44 | 45 | if(isProd) { 46 | stream.pipe(streamify(uglify())); 47 | } 48 | stream.pipe(gulp.dest('./dist')) 49 | }); 50 | 51 | gulp.task('sass', function () { 52 | return gulp.src(files.scss) 53 | .pipe(sass()) 54 | .pipe(minifyCSS()) 55 | .pipe(gulp.dest("build")); 56 | }); 57 | 58 | gulp.task("copy-css", function() { 59 | return gulp.src(files.css) 60 | .pipe(minifyCSS()) 61 | .pipe(gulp.dest('build')); 62 | }); 63 | 64 | gulp.task("copy-html", function() { 65 | return gulp.src(files.html) 66 | .pipe(gulp.dest('build')); 67 | }); 68 | 69 | gulp.task('serve', ['watch'], function() { 70 | express() 71 | .use(express.static('.')) 72 | .use(express.static('./dist')) 73 | .listen(3000, "0.0.0.0"); 74 | util.log(util.colors.bgGreen('Listening on port 3000')); 75 | }); 76 | 77 | 78 | gulp.task('watch', function() { 79 | var reloadServer = livereload(); 80 | 81 | var js = gulp.watch('./*.js'); 82 | js.on('change', function(event) { 83 | gulp.start('build', function() { 84 | reloadServer.changed(event.path); 85 | }); 86 | }); 87 | 88 | var assets = gulp.watch('./public/*.{html,scss,css}'); 89 | assets.on('change', function(event) { 90 | gulp.start(['sass', 'copy-html', 'copy-css'], function() { 91 | reloadServer.changed(event.path); 92 | }); 93 | }); 94 | util.log(util.colors.bgGreen('Watching for changes...')); 95 | }); 96 | 97 | 98 | gulp.task('upload', function(b) { 99 | var awsOptions = JSON.parse(fs.readFileSync('./aws.json')); 100 | var client = s3.createClient({s3Options: awsOptions}); 101 | var params = {localDir: "./dist/", deleteRemoved: true, s3Params: {Bucket: "cdn.clappr.io", Prefix: "bemtv/latest/"}}; 102 | var uploader = client.uploadDir(params); 103 | uploader.on('error', function(err) { console.error("unable to sync:", err.stack); }); 104 | uploader.on('end', function() { console.log("done uploading"); }); 105 | return; 106 | }); 107 | 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Flávio Ribeiro 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BemTV Plugin for Clappr Media Player 2 | 3 | [![Build Status](https://travis-ci.org/bemtv/clappr-p2phls-plugin.svg?branch=master)](https://travis-ci.org/bemtv/clappr-p2phls-plugin) 4 | [![Coverage Status](https://img.shields.io/coveralls/bemtv/clappr-p2phls-plugin.svg)](https://coveralls.io/r/bemtv/clappr-p2phls-plugin) 5 | [![bemtv google group](http://img.shields.io/badge/discuss-bemtv-blue.svg)](https://groups.google.com/forum/#!forum/bemtv) 6 | [![Issue Stats](http://issuestats.com/github/bemtv/clappr-p2phls-plugin/badge/issue)](http://issuestats.com/github/bemtv/clappr-p2phls-plugin) 7 | 8 | This plugin adds peer-to-peer powers for HTTP Live Streaming (HLS) transmissions on [Clappr Player](http://github.com/globocom/clappr). 9 | 10 | ![BemTV P2PHLS](https://cloud.githubusercontent.com/assets/244265/4802042/33f02800-5e3d-11e4-8a82-50bd3af76526.png) 11 | 12 | # Try it now! 13 | 14 | Visit [BemTV](http://bem.tv) with a [modern browser](http://caniuse.com/#search=webrtc) and try it by yourself. 15 | 16 | # How to Use 17 | 18 | ```html 19 | 20 | 21 | 22 | 23 | 24 |
25 | 35 | 36 | ``` 37 | 38 | # Questions/Support? 39 | 40 | Post your question at our Google Groups discussion list: https://groups.google.com/d/forum/bemtv 41 | 42 | # Contribute 43 | 44 | If you'd like to support the development of this project, consider make a donation. 45 | 46 | [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=BWQTD9JLRTNF6&lc=US&item_name=BemTV%20CDN%2fP2P%20Architecture%20for%20HLS%20Broadcasts&item_number=bemtv¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted) 47 | 48 | 49 | # Author 50 | 51 | [Flávio Ribeiro](https://www.linkedin.com/in/flavioribeiro) (flavio@bem.tv) 52 | 53 | ![BemTV](http://bem.tv/static/bemtv_small_logo.png) 54 | -------------------------------------------------------------------------------- /bin/.hook_template: -------------------------------------------------------------------------------- 1 | //This file is generated by bin/hook.js 2 | var _ = require('underscore'); 3 | module.exports = { 4 | <% _.each(templates, function(template) { %> 5 | '<%= template.name %>': _.template('<%= template.content %>'), 6 | <% }); %> 7 | CSS: { 8 | <% _.each(styles, function(style) { %> 9 | '<%= style.name %>': '<%= style.content %>', 10 | <% }); %> 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /bin/hook.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Clappr authors. All rights reserved. 2 | // Use of this source code is governed by a Apache 3 | // license that can be found in the LICENSE file. 4 | 5 | var glob = require('glob').sync; 6 | var mkdirp = require('mkdirp').sync; 7 | var path = require('path'); 8 | var fs = require('fs'); 9 | var _ = require('underscore'); 10 | 11 | var codeTemplate = _.template(fs.readFileSync('bin/.hook_template').toString()); 12 | 13 | var jstFile = './src/jst.js'; 14 | 15 | function format(filePath) { 16 | var content = fs.readFileSync(filePath).toString().replace(/\r?\n|\r/g, ''); 17 | return {name: 'p2phls', content: content}; 18 | } 19 | 20 | function copyFiles(asset) { 21 | var targetDir = path.extname(asset) === '.js' ? 'dist/' : 'dist/assets'; 22 | fs.createReadStream(asset) 23 | .pipe(fs.createWriteStream(path.join(targetDir, path.basename(asset)))); 24 | } 25 | 26 | var templates = glob('build/**/*.html').map(format); 27 | var styles = glob('build/**/*.css').map(format); 28 | 29 | fs.writeFileSync(jstFile, codeTemplate({templates: templates, styles: styles})); 30 | 31 | mkdirp('dist/assets/'); 32 | 33 | glob('./node_modules/clappr/dist/**/*.{png,jpeg,jpg,gif,swf,eot,ttf,svg}').map(copyFiles); 34 | glob('public/*.{png,jpeg,jpg,gif,swf,eot,ttf,svg,js}').map(copyFiles); 35 | glob('./node_modules/clappr/dist/*.js').map(copyFiles); 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Test Page 11 | 28 | 29 | 30 |
31 | 32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by Apache 4 | // license that can be found in the LICENSE file. 5 | // 6 | module.exports = require("./src/main"); 7 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Oct 25 2014 17:11:56 GMT-0200 (BRST) 3 | 4 | var dotenv = require('dotenv'); 5 | var exec = require('child_process').exec; 6 | exec('node bin/hook.js'); 7 | dotenv.load(); 8 | 9 | module.exports = function(config) { 10 | config.set({ 11 | 12 | // base path that will be used to resolve all patterns (eg. files, exclude) 13 | basePath: '', 14 | 15 | 16 | // frameworks to use 17 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 18 | frameworks: ['browserify', 'traceur', 'mocha', 'sinon-chai'], 19 | 20 | 21 | // list of files / patterns to load in the browser 22 | files: [ 23 | 'node_modules/underscore/underscore-min.js', 24 | 'node_modules/clappr/dist/clappr.js', 25 | 'test/**/*spec.js' 26 | ], 27 | 28 | 29 | // list of files to exclude 30 | exclude: [ 31 | ], 32 | 33 | 34 | // preprocess matching files before serving them to the browser 35 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 36 | preprocessors: { 37 | './index.js': ['traceur'], 38 | 'src/**/*.js': ['traceur'], 39 | 'test/**/*.js': ['traceur', 'browserify'] 40 | }, 41 | 42 | traceurPreprocessor: { 43 | options: { 44 | sourceMap: true 45 | } 46 | }, 47 | 48 | browserify: { 49 | debug: true, 50 | transform: ['es6ify'], 51 | prebundle: function(bundle) { 52 | bundle.external('base_object'); 53 | bundle.external('underscore'); 54 | }, 55 | }, 56 | 57 | // test results reporter to use 58 | // possible values: 'dots', 'progress' 59 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 60 | reporters: ['progress'], 61 | 62 | 63 | // web server port 64 | port: 9876, 65 | 66 | 67 | // enable / disable colors in the output (reporters and logs) 68 | colors: true, 69 | 70 | 71 | // level of logging 72 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 73 | logLevel: config.LOG_INFO, 74 | 75 | 76 | // enable / disable watching file and executing tests whenever any file changes 77 | autoWatch: true, 78 | 79 | 80 | // start these browsers 81 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 82 | browsers: ['Chrome'], 83 | 84 | 85 | // Continuous Integration mode 86 | // if true, Karma captures browsers, runs the tests and exits 87 | singleRun: false 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p2phls", 3 | "version": "0.1.8", 4 | "description": "BemTV Peer-to-Peer plugin for HTTP Live Streaming transmissions on Clappr Media Player", 5 | "main": "dist/p2phls.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/gulp build && ./node_modules/.bin/gulp build --env=prod && ./node_modules/.bin/karma start --single-run --browsers Firefox" 8 | }, 9 | "author": "Flávio Ribeiro", 10 | "keywords": [ 11 | "p2p", 12 | "hls", 13 | "http live streaming", 14 | "player", 15 | "online video", 16 | "streaming", 17 | "peer-to-peer" 18 | ], 19 | "homepage": "http://bem.tv", 20 | "bugs": { 21 | "url": "http://github.com/bemtv/clappr-p2phls-plugin" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:bemtv/clappr-p2phls-plugin.git" 26 | }, 27 | "devDependencies": { 28 | "underscore" : "1.7.0", 29 | "s3": "^4.1.1", 30 | "browserify": "^6.3.2", 31 | "chai": "latest", 32 | "dotenv": "^0.4.0", 33 | "es6ify": "1.4.0", 34 | "express": "^4.6.1", 35 | "glob": "^4.0.4", 36 | "gulp": "^3.8.6", 37 | "gulp-uglify": "0.3.1", 38 | "gulp-livereload": "^2.1.0", 39 | "gulp-streamify": "0.0.5", 40 | "gulp-minify-css": "^0.3.6", 41 | "gulp-rename": "^1.2.0", 42 | "gulp-sass": "^0.7.2", 43 | "gulp-util": "latest", 44 | "mkdirp": "^0.5.0", 45 | "yargs": "latest", 46 | "karma": "latest", 47 | "karma-browserify": "^1.0.0", 48 | "karma-chai": "^0.1.0", 49 | "karma-chrome-launcher": "^0.1.4", 50 | "karma-cli": "0.0.4", 51 | "karma-firefox-launcher": "^0.1.3", 52 | "karma-jasmine": "^0.2.2", 53 | "karma-mocha": "^0.1.4", 54 | "karma-safari-launcher": "^0.1.1", 55 | "karma-sinon": "^1.0.3", 56 | "karma-sinon-chai": "^0.2.0", 57 | "sinon": "^1.10.2", 58 | "traceur": "0.0.72", 59 | "vinyl-source-stream": "^0.1.1", 60 | "karma-traceur-preprocessor": "0.4.0" 61 | }, 62 | "dependencies": { 63 | "clappr": "latest", 64 | "rtc-bufferedchannel": "latest", 65 | "rtc-quickconnect": "2.0.0", 66 | "log-with-style": "0.1.5", 67 | "mousetrap": "0.0.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamroot/clappr-p2phls-plugin/6a2b82af02964abfa95e8e3122ec4e9a08516cb4/public/.gitkeep -------------------------------------------------------------------------------- /public/P2PHLSPlayer.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamroot/clappr-p2phls-plugin/6a2b82af02964abfa95e8e3122ec4e9a08516cb4/public/P2PHLSPlayer.swf -------------------------------------------------------------------------------- /public/p2phls_playback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 24 | 25 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | [data-phphls] { 2 | position: absolute; 3 | height: 100%; 4 | width: 100%; 5 | background-color: black; 6 | display: block; 7 | pointer-events: none; 8 | } 9 | -------------------------------------------------------------------------------- /src/adaptive_streaming.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object') 7 | var log = require('./log').getInstance() 8 | 9 | class AdaptiveStreaming extends BaseObject { 10 | constructor(main) { 11 | Clappr.Mediator.on(main.uniqueId + ':fragmentloaded', () => this.onFragmentLoaded()) 12 | this.main = main 13 | this.info = this.main.playbackInfo.data 14 | this.currentLevel = 0 15 | this.threshold = 1.2 16 | } 17 | 18 | onFragmentLoaded() { 19 | this.adjustLevel() 20 | } 21 | 22 | adjustLevel() { 23 | var idealLevel = this.calculateIdealLevel() 24 | if (this.info.lastDownloadType === 'cdn' && this.currentLevel !== idealLevel) { 25 | log.info("Changing level: " + this.currentLevel + ' (' + this.info.currentBitrate + "Kbps) -> " + idealLevel + ' (' + this.info.levels[idealLevel].bitrate/1000 + "Kbps)") 26 | this.changeLevel(idealLevel) 27 | } 28 | } 29 | 30 | calculateIdealLevel() { 31 | var idealLevel = 0 32 | for (var i = 0; i < this.info.levels.length; i++) { 33 | var bitrate = this.info.levels[i].bitrate 34 | var bwNeeded = bitrate * this.threshold / 1000 35 | if (this.info.bandwidth > bwNeeded && bitrate < 1500000) { 36 | idealLevel = i 37 | } 38 | } 39 | return idealLevel 40 | } 41 | 42 | changeLevel(newLevel) { 43 | this.currentLevel = newLevel 44 | this.main.el.globoPlayerSmoothSetLevel(newLevel) 45 | } 46 | } 47 | 48 | 49 | module.exports = AdaptiveStreaming 50 | -------------------------------------------------------------------------------- /src/cdn_requester.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object') 7 | var Settings = require('./settings') 8 | var log = require('./log').getInstance() 9 | 10 | class CDNRequester extends BaseObject { 11 | get name() { return 'CDNRequester' } 12 | constructor() { 13 | this.utils = new Worker(this.getWorkerURL()) 14 | this.utils.onmessage = (e) => this.resourceLoaded(e.data) 15 | } 16 | 17 | getWorkerURL() { 18 | // non-minified version at https://gist.github.com/flavioribeiro/d706afbf83d055439f21 19 | var content = 'request=function(e,r,t){var n=new XMLHttpRequest;r&&(n.withCredentials=!0),n.open("GET",e,t?!0:!1),n.responseType="arraybuffer",n.onload=function(e){t(200===n.status?e.currentTarget.response:"")},n.send()},base64ArrayBuffer=function(e){for(var r,t,n,s,a,o="",u="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",i=new Uint8Array(e),d=i.byteLength,f=d%3,c=d-f,p=0;c>p;p+=3)a=i[p]<<16|i[p+1]<<8|i[p+2],r=(16515072&a)>>18,t=(258048&a)>>12,n=(4032&a)>>6,s=63&a,o+=u[r]+u[t]+u[n]+u[s];return 1==f?(a=i[c],r=(252&a)>>2,t=(3&a)<<4,o+=u[r]+u[t]+"=="):2==f&&(a=i[c]<<8|i[c+1],r=(64512&a)>>10,t=(1008&a)>>4,n=(15&a)<<2,o+=u[r]+u[t]+u[n]+"="),o},resourceLoaded=function(e){var e=base64ArrayBuffer(e);this.postMessage(e)},this.addEventListener("message",function(e){var r=JSON.parse(e.data);request(r.url,r.allowCredentials,resourceLoaded.bind(this))},!1);' 20 | var blob 21 | try { 22 | blob = new Blob([content], {type: 'application/javascript'}) 23 | } catch (e) { 24 | window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder 25 | blob = new BlobBuilder() 26 | blob.append(content) 27 | blob = blob.getBlob() 28 | } 29 | 30 | return URL.createObjectURL(blob) 31 | } 32 | 33 | requestResource(resource, callback) { 34 | this.callback = callback 35 | this.resource = resource 36 | var message = {'url':resource, 'allowCredentials': Settings.forceAllowCredentials} 37 | this.utils.postMessage(JSON.stringify(message)) 38 | this.startRequest = Date.now() 39 | } 40 | 41 | resourceLoaded(chunk) { 42 | if (chunk.length === 0) { 43 | log.warn("error fetching from CDN, retrying") 44 | this.requestResource(this.resource, this.callback) 45 | } else { 46 | var downloadTime = Date.now() - this.startRequest 47 | this.trigger('cdnrequester:downloadtime', {downloadTime: downloadTime, type: 'cdn'}) 48 | this.callback(chunk, "cdn"); 49 | } 50 | } 51 | } 52 | 53 | module.exports = CDNRequester; 54 | -------------------------------------------------------------------------------- /src/flash_uploader.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | // This module is responsible for sending subsegments 7 | // of a given chunk in order to avoid overhead or stucks 8 | // on javascript interpreter. 9 | 10 | class FlashUploader { 11 | constructor() { 12 | this.MAX_SIZE = 65536 13 | this.readPosition = 0 14 | this.endPosition = 0 15 | } 16 | 17 | send(sendPartCallback, chunk, finishSendingCallback) { 18 | this.chunk = chunk 19 | this.currentChunkLength = chunk.length 20 | this.sendPartCallback = sendPartCallback 21 | this.finishCallback = finishSendingCallback 22 | this.sendID = setInterval(this.sendChunk.bind(this), 0); 23 | } 24 | 25 | sendChunk() { 26 | if (this.currentChunkLength <= this.MAX_SIZE) { 27 | this.sendPartCallback(this.chunk) 28 | this.startDecoding() 29 | } else if (this.endPosition >= this.currentChunkLength) { 30 | this.startDecoding() 31 | } else { 32 | this.endPosition += this.MAX_SIZE 33 | this.sendPartCallback(this.chunk.slice(this.readPosition, this.endPosition)) 34 | this.readPosition = this.endPosition 35 | } 36 | } 37 | 38 | startDecoding() { 39 | this.readPosition = 0 40 | this.endPosition = 0 41 | clearInterval(this.sendID) 42 | this.finishCallback() 43 | } 44 | } 45 | 46 | module.exports = FlashUploader 47 | -------------------------------------------------------------------------------- /src/jst.js: -------------------------------------------------------------------------------- 1 | //This file is generated by bin/hook.js 2 | var _ = require('underscore'); 3 | module.exports = { 4 | 5 | 'p2phls': _.template(' '), 6 | 7 | CSS: { 8 | 9 | 'p2phls': '[data-phphls]{position:absolute;height:100%;width:100%;background-color:#000;display:block;pointer-events:none}', 10 | 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var Settings = require('./settings'); 7 | var Mousetrap = require('mousetrap'); 8 | 9 | class Log { 10 | constructor() { 11 | Mousetrap.bind(['command+shift+l', 'ctrl+shift+l'], () => this.onOff()) 12 | } 13 | 14 | info(message) {this.log('info', message)} 15 | warn(message) {this.log('warn', message)} 16 | debug(message) {this.log('debug', message)} 17 | good(message) {this.log('good', message)} 18 | bad(message) {this.log('bad', message)} 19 | 20 | onOff() { 21 | Settings.logging = !Settings.logging 22 | if (Settings.logging) console.log('%c [INFO] log enabled', 'color: blue') 23 | else console.log('%c [INFO] log disabled', 'color: blue') 24 | } 25 | 26 | log(level, message) { 27 | if (!Settings.logging) return 28 | var color, prefix 29 | if (level === 'warn') { [color, prefix] = ['red', 'WARN'] } 30 | if (level === 'info') { [color, prefix] = ['green', 'INFO'] } 31 | if (level === 'debug') { [color, prefix] = ['blue', 'DEBUG'] } 32 | if (level === 'good') { [color, prefix] = ['green', 'GOOD'] } 33 | if (level === 'bad') { [color, prefix] = ['red', 'BAD'] } 34 | console.log('%c [' + prefix + '] ' + message, 'color:' + color) 35 | } 36 | } 37 | 38 | Log.getInstance = function() { 39 | if (this._instance === undefined) { 40 | this._instance = new this() 41 | } 42 | return this._instance 43 | } 44 | 45 | 46 | module.exports = Log 47 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var Settings = require('./settings') 7 | var ResourceRequester = require('./resource_requester') 8 | var UploadHandler = require('./upload_handler') 9 | var PlaybackInfo = require('./playback_info') 10 | var AdaptiveStreaming = require('./adaptive_streaming') 11 | var Storage = require('./storage') 12 | var FlashUploader = require('./flash_uploader') 13 | var log = require('./log').getInstance() 14 | var JST = require('./jst') 15 | var Browser = require('browser') 16 | var Styler = require('./styler') 17 | var HLS = require('hls') 18 | var version = require('../package.json').version 19 | var _ = require('underscore') 20 | 21 | class P2PHLS extends HLS { 22 | get name() { return 'p2phls' } 23 | get tagName() { return 'object' } 24 | get template() { return JST.p2phls } 25 | get attributes() { 26 | return { 27 | 'data-p2phls': '', 28 | 'type': 'application/x-shockwave-flash' 29 | } 30 | } 31 | 32 | constructor(options) { 33 | Settings = _.extend(Settings, options.bemtv) 34 | this.resourceRequester = new ResourceRequester({swarm: btoa(options.src.split("?")[0]), tracker: options.tracker}) 35 | this.uploadHandler = UploadHandler.getInstance() 36 | this.playbackInfo = PlaybackInfo.getInstance() 37 | this.storage = Storage.getInstance() 38 | super(options) 39 | this.swfPath = "http://cdn.clappr.io/bemtv/latest/assets/P2PHLSPlayer.swf" 40 | } 41 | 42 | addListeners() { 43 | Clappr.Mediator.on(this.uniqueId + ':flashready', () => this.bootstrap()) 44 | Clappr.Mediator.on(this.uniqueId + ':timeupdate', () => this.updateTime()) 45 | Clappr.Mediator.on(this.uniqueId + ':playbackstate', (state) => this.setPlaybackState(state)) 46 | Clappr.Mediator.on(this.uniqueId + ':highdefinition', (isHD) => this.updateHighDefinition(isHD)) 47 | Clappr.Mediator.on(this.uniqueId + ':playbackerror', () => this.flashPlaybackError()) 48 | Clappr.Mediator.on(this.uniqueId + ':requestresource', (url) => this.requestResource(url)) 49 | Clappr.Mediator.on(this.uniqueId + ':decodeerror', () => this.onDecodeError()) 50 | Clappr.Mediator.on(this.uniqueId + ':decodesuccess', () => this.onDecodeSuccess()) 51 | } 52 | 53 | stopListening() { 54 | Clappr.Mediator.off(this.uniqueId + ':flashready') 55 | Clappr.Mediator.off(this.uniqueId + ':timeupdate') 56 | Clappr.Mediator.off(this.uniqueId + ':playbackstate') 57 | Clappr.Mediator.off(this.uniqueId + ':highdefinition') 58 | Clappr.Mediator.off(this.uniqueId + ':playbackerror') 59 | Clappr.Mediator.off(this.uniqueId + ':requestresource') 60 | Clappr.Mediator.off(this.uniqueId + ':decodeerror') 61 | Clappr.Mediator.off(this.uniqueId + ':decodesuccess') 62 | } 63 | 64 | bootstrap() { 65 | this.playbackInfo.setMain(this) 66 | this.adaptiveStreaming = new AdaptiveStreaming(this) 67 | this.el.playerSetminBufferLength(6) 68 | this.el.playerSetlowBufferLength(Settings.lowBufferLength) 69 | super() 70 | } 71 | 72 | setPlaybackState(state) { 73 | if (state === 'PLAYING' && this.resourceRequester.isInitialBuffer) { 74 | this.resourceRequester.isInitialBuffer = false 75 | } 76 | super(state) 77 | } 78 | 79 | onDecodeError() { 80 | log.warn("Error, decode error") 81 | this.resourceRequester.decodingError = true 82 | this.resourceRequester.requestResource(this.currentUrl, 0, (chunk, method) => this.resourceLoaded(chunk, method)) 83 | } 84 | 85 | onDecodeSuccess() { 86 | if (this.currentUrl) { 87 | this.resourceRequester.decodingError = false 88 | this.storage.setItem(this.currentUrl, this.currentChunk) 89 | this.currentUrl = null 90 | this.currentChunk = null 91 | } 92 | } 93 | 94 | requestResource(url) { 95 | if (this.currentUrl) { 96 | log.warn("still processing the other chunk, wait :)") 97 | } else { 98 | this.currentUrl = url 99 | if (this.storage.contain(this.currentUrl)) { 100 | this.resourceLoaded(this.storage.getItem(this.currentUrl), "storage") 101 | } else { 102 | this.resourceRequester.requestResource(url, this.el.globoGetbufferLength(), (chunk, method) => this.resourceLoaded(chunk, method)) 103 | } 104 | } 105 | } 106 | 107 | resourceLoaded(chunk, method) { 108 | if (this.currentUrl) { 109 | this.currentChunk = chunk 110 | this.flashUploader = new FlashUploader() 111 | var sendPartCallback = function(part) { this.el.resourceLoaded(part) }.bind(this) 112 | var finishSendingCallback = function() { this.el.startDecoding() }.bind(this) 113 | this.flashUploader.send(sendPartCallback, chunk, finishSendingCallback) 114 | this.playbackInfo.updateChunkStats(method) 115 | } 116 | } 117 | 118 | seek(time) { 119 | this.resourceRequester.onDVR = time !== -1? true: false 120 | console.log("onDVR", this.resourceRequester.onDVR) 121 | super(time) 122 | } 123 | 124 | render() { 125 | this.$el.html(this.template({cid: this.cid, swfPath: this.swfPath, playbackId: this.uniqueId})) 126 | if(Browser.isFirefox) { 127 | this.setupFirefox() 128 | } 129 | this.el.id = this.cid 130 | var style = Styler.getStyleFor(this.name) 131 | this.$el.append(style) 132 | return this 133 | } 134 | } 135 | 136 | P2PHLS.canPlay = function(resource) { 137 | return !!(window.webkitRTCPeerConnection || window.mozRTCPeerConnection) && !!resource.match(/^http(.*).m3u8/) 138 | } 139 | 140 | P2PHLS.version = version 141 | 142 | module.exports = window.P2PHLS = P2PHLS; 143 | -------------------------------------------------------------------------------- /src/md5.js: -------------------------------------------------------------------------------- 1 | // from http://www.myersdaily.org/joseph/javascript/md5-text.html 2 | function add32(a, b) { 3 | return (a + b) & 0xFFFFFFFF; 4 | } 5 | 6 | function cmn(q, a, b, x, s, t) { 7 | a = add32(add32(a, q), add32(x, t)); 8 | return add32((a << s) | (a >>> (32 - s)), b); 9 | } 10 | 11 | function ff(a, b, c, d, x, s, t) { 12 | return cmn((b & c) | ((~b) & d), a, b, x, s, t); 13 | } 14 | 15 | function gg(a, b, c, d, x, s, t) { 16 | return cmn((b & d) | (c & (~d)), a, b, x, s, t); 17 | } 18 | 19 | function hh(a, b, c, d, x, s, t) { 20 | return cmn(b ^ c ^ d, a, b, x, s, t); 21 | } 22 | 23 | function ii(a, b, c, d, x, s, t) { 24 | return cmn(c ^ (b | (~d)), a, b, x, s, t); 25 | } 26 | 27 | function md5cycle(x, k) { 28 | var a = x[0], b = x[1], c = x[2], d = x[3]; 29 | 30 | a = ff(a, b, c, d, k[0], 7, -680876936); 31 | d = ff(d, a, b, c, k[1], 12, -389564586); 32 | c = ff(c, d, a, b, k[2], 17, 606105819); 33 | b = ff(b, c, d, a, k[3], 22, -1044525330); 34 | a = ff(a, b, c, d, k[4], 7, -176418897); 35 | d = ff(d, a, b, c, k[5], 12, 1200080426); 36 | c = ff(c, d, a, b, k[6], 17, -1473231341); 37 | b = ff(b, c, d, a, k[7], 22, -45705983); 38 | a = ff(a, b, c, d, k[8], 7, 1770035416); 39 | d = ff(d, a, b, c, k[9], 12, -1958414417); 40 | c = ff(c, d, a, b, k[10], 17, -42063); 41 | b = ff(b, c, d, a, k[11], 22, -1990404162); 42 | a = ff(a, b, c, d, k[12], 7, 1804603682); 43 | d = ff(d, a, b, c, k[13], 12, -40341101); 44 | c = ff(c, d, a, b, k[14], 17, -1502002290); 45 | b = ff(b, c, d, a, k[15], 22, 1236535329); 46 | 47 | a = gg(a, b, c, d, k[1], 5, -165796510); 48 | d = gg(d, a, b, c, k[6], 9, -1069501632); 49 | c = gg(c, d, a, b, k[11], 14, 643717713); 50 | b = gg(b, c, d, a, k[0], 20, -373897302); 51 | a = gg(a, b, c, d, k[5], 5, -701558691); 52 | d = gg(d, a, b, c, k[10], 9, 38016083); 53 | c = gg(c, d, a, b, k[15], 14, -660478335); 54 | b = gg(b, c, d, a, k[4], 20, -405537848); 55 | a = gg(a, b, c, d, k[9], 5, 568446438); 56 | d = gg(d, a, b, c, k[14], 9, -1019803690); 57 | c = gg(c, d, a, b, k[3], 14, -187363961); 58 | b = gg(b, c, d, a, k[8], 20, 1163531501); 59 | a = gg(a, b, c, d, k[13], 5, -1444681467); 60 | d = gg(d, a, b, c, k[2], 9, -51403784); 61 | c = gg(c, d, a, b, k[7], 14, 1735328473); 62 | b = gg(b, c, d, a, k[12], 20, -1926607734); 63 | 64 | a = hh(a, b, c, d, k[5], 4, -378558); 65 | d = hh(d, a, b, c, k[8], 11, -2022574463); 66 | c = hh(c, d, a, b, k[11], 16, 1839030562); 67 | b = hh(b, c, d, a, k[14], 23, -35309556); 68 | a = hh(a, b, c, d, k[1], 4, -1530992060); 69 | d = hh(d, a, b, c, k[4], 11, 1272893353); 70 | c = hh(c, d, a, b, k[7], 16, -155497632); 71 | b = hh(b, c, d, a, k[10], 23, -1094730640); 72 | a = hh(a, b, c, d, k[13], 4, 681279174); 73 | d = hh(d, a, b, c, k[0], 11, -358537222); 74 | c = hh(c, d, a, b, k[3], 16, -722521979); 75 | b = hh(b, c, d, a, k[6], 23, 76029189); 76 | a = hh(a, b, c, d, k[9], 4, -640364487); 77 | d = hh(d, a, b, c, k[12], 11, -421815835); 78 | c = hh(c, d, a, b, k[15], 16, 530742520); 79 | b = hh(b, c, d, a, k[2], 23, -995338651); 80 | 81 | a = ii(a, b, c, d, k[0], 6, -198630844); 82 | d = ii(d, a, b, c, k[7], 10, 1126891415); 83 | c = ii(c, d, a, b, k[14], 15, -1416354905); 84 | b = ii(b, c, d, a, k[5], 21, -57434055); 85 | a = ii(a, b, c, d, k[12], 6, 1700485571); 86 | d = ii(d, a, b, c, k[3], 10, -1894986606); 87 | c = ii(c, d, a, b, k[10], 15, -1051523); 88 | b = ii(b, c, d, a, k[1], 21, -2054922799); 89 | a = ii(a, b, c, d, k[8], 6, 1873313359); 90 | d = ii(d, a, b, c, k[15], 10, -30611744); 91 | c = ii(c, d, a, b, k[6], 15, -1560198380); 92 | b = ii(b, c, d, a, k[13], 21, 1309151649); 93 | a = ii(a, b, c, d, k[4], 6, -145523070); 94 | d = ii(d, a, b, c, k[11], 10, -1120210379); 95 | c = ii(c, d, a, b, k[2], 15, 718787259); 96 | b = ii(b, c, d, a, k[9], 21, -343485551); 97 | 98 | x[0] = add32(a, x[0]); 99 | x[1] = add32(b, x[1]); 100 | x[2] = add32(c, x[2]); 101 | x[3] = add32(d, x[3]); 102 | 103 | } 104 | 105 | function md5blk(s) { /* I figured global was faster. */ 106 | var md5blks = [], i; /* Andy King said do it this way. */ 107 | for (i=0; i<64; i+=4) { 108 | md5blks[i>>2] = s.charCodeAt(i) + (s.charCodeAt(i+1) << 8) + (s.charCodeAt(i+2) << 16) + (s.charCodeAt(i+3) << 24); 109 | } 110 | return md5blks; 111 | } 112 | 113 | function md51(s) { 114 | var n = s.length, 115 | state = [1732584193, -271733879, -1732584194, 271733878], i; 116 | for (i=64; i<=s.length; i+=64) { 117 | md5cycle(state, md5blk(s.substring(i-64, i))); 118 | } 119 | s = s.substring(i-64); 120 | var tail = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]; 121 | for (i=0; i>2] |= s.charCodeAt(i) << ((i%4) << 3); 123 | } 124 | tail[i>>2] |= 0x80 << ((i%4) << 3); 125 | if (i > 55) { 126 | md5cycle(state, tail); 127 | for (i=0; i<16; i++) { 128 | tail[i] = 0; 129 | } 130 | } 131 | tail[14] = n*8; 132 | md5cycle(state, tail); 133 | return state; 134 | } 135 | 136 | var hex_chr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]; 137 | 138 | function rhex(n) { 139 | var s=''; 140 | var j; 141 | for(j=0; j<4; j++) { 142 | s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; 143 | } 144 | return s; 145 | } 146 | 147 | function hex(x) { 148 | var i; 149 | for (i=0; i. 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object'); 7 | 8 | var QuickConnect = require('rtc-quickconnect'); 9 | var Settings = require("./settings") 10 | var Swarm = require('./swarm') 11 | var log = require('./log').getInstance() 12 | var _ = require('underscore') 13 | 14 | class P2PManager extends BaseObject { 15 | constructor(params) { 16 | this.connectionSettings = {'room': params.swarm, iceServers: Settings.stunServers, debug: false} 17 | log.info("P2P active, connected to " + Settings.tracker) 18 | var connection = QuickConnect(Settings.tracker, this.connectionSettings) 19 | this.swarm = new Swarm() 20 | this.dataChannel = connection.createDataChannel('bemtv') 21 | this.setupListerners() 22 | } 23 | 24 | setupListerners() { 25 | this.dataChannel.on('channel:opened', (id, dataChannel) => this.onChannelOpened(id, dataChannel)) 26 | this.dataChannel.on('channel:closed', (id, dataChannel) => this.onChannelClosed(id)) 27 | } 28 | 29 | onChannelOpened(id, dataChannel) { 30 | if (this.swarm.size() <= Settings.maxSwarmSize) { 31 | this.swarm.addPeer(id, dataChannel) 32 | } else { 33 | log.warn("ignoring new peer, maxSwarmSize reached.") 34 | } 35 | } 36 | 37 | onChannelClosed(id) { 38 | this.swarm.removePeer(id) 39 | } 40 | 41 | requestResource(resource, callbackSuccess, callbackFail) { 42 | if (_.size(this.swarm.utils.contributors) === 0) { 43 | callbackFail() 44 | } else { 45 | this.swarm.sendInterested(resource, callbackSuccess, callbackFail) 46 | } 47 | } 48 | } 49 | 50 | module.exports = P2PManager 51 | -------------------------------------------------------------------------------- /src/peer.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object'); 7 | var Storage = require('./storage'); 8 | var UploadHandler = require('./upload_handler') 9 | var PlaybackInfo = require('./playback_info') 10 | var log = require('./log').getInstance() 11 | var md5 = require('./md5') 12 | 13 | class Peer extends BaseObject { 14 | constructor(params) { 15 | this.storage = Storage.getInstance() 16 | this.ident = params.ident 17 | this.swarm = params.swarm 18 | this.dataChannel = params.dataChannel 19 | this.dataChannel.on("data", (data) => this.messageReceived(data)) 20 | this.uploadHandler = UploadHandler.getInstance() 21 | this.playbackInfo = PlaybackInfo.getInstance() 22 | this.score = 1000 23 | this.late = 0 24 | this.active = false 25 | this.sendPing() 26 | } 27 | 28 | sendPing() { 29 | this.pingSent = Date.now() 30 | this.dataChannel.send("ping$$" + (new Array(2 * 1024)).join("x")) 31 | } 32 | 33 | sendPong() { 34 | this.dataChannel.send("pong$$") 35 | } 36 | 37 | pongReceived() { 38 | var rtt = Date.now() - this.pingSent 39 | this.active = true 40 | this.score -= Math.ceil(rtt / 100) 41 | log.info('join: ' + this.ident + " (rtt: " + rtt + ")") 42 | } 43 | 44 | sendSatisfy(resource) { 45 | if (this.storage.contain(resource)) { 46 | if (this.uploadHandler.getSlot(this.ident)) { 47 | var content = this.storage.getItem(resource) 48 | content = md5(content) + content 49 | this.send('satisfy', resource, content) 50 | this.playbackInfo.updateChunkStats('p2psent') 51 | } else { 52 | log.warn("cannot send satisfy, no upload slot available") 53 | this.send("busy", resource) 54 | } 55 | } else { 56 | this.send('choke', resource) 57 | } 58 | } 59 | 60 | interestedReceived(resource) { 61 | if (this.storage.contain(resource)) { 62 | if (this.uploadHandler.getSlot(this.ident)) { 63 | this.send('contain', resource) 64 | } else { 65 | this.send('busy', resource) 66 | } 67 | } else { 68 | this.send("choke", resource) 69 | } 70 | } 71 | 72 | messageReceived(data) { 73 | var [command, resource, content] = data.split("$") 74 | switch (command) { 75 | case 'interested': 76 | this.interestedReceived(resource) 77 | break 78 | case 'contain': 79 | this.swarm.containReceived(this, resource) 80 | break 81 | case 'request': 82 | this.sendSatisfy(resource) 83 | break 84 | case 'choke': 85 | this.swarm.chokeReceived(resource) 86 | break 87 | case 'satisfy': 88 | var md5Header = content.slice(0, 32) 89 | var realContent = content.slice(32) 90 | if (content.length > 0 && md5Header === md5(realContent)) { 91 | log.info("received satisfy, md5 ok") 92 | this.swarm.satisfyReceived(this, resource, realContent) 93 | } else { 94 | log.warn("error receiving segment") 95 | } 96 | break 97 | case 'busy': 98 | this.swarm.busyReceived(this) 99 | break 100 | case 'ping': 101 | this.sendPong() 102 | break 103 | case 'pong': 104 | this.pongReceived(this) 105 | break 106 | } 107 | } 108 | 109 | send(command, resource, content='') { 110 | var message = this.mountMessage(command, resource, content) 111 | this.dataChannel.send(message) 112 | } 113 | 114 | mountMessage(command, resource, content) { 115 | var msg = command + "$" + resource + "$" 116 | if (content) { 117 | msg = msg + content 118 | } 119 | return msg 120 | } 121 | } 122 | 123 | module.exports = Peer 124 | -------------------------------------------------------------------------------- /src/playback_info.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object') 7 | var Settings = require('./settings') 8 | var _ = require('underscore') 9 | 10 | class PlaybackInfo extends BaseObject { 11 | constructor() { 12 | this.data = { 13 | 'chunks': {chunksFromCDN: 0, chunksFromP2P: 0, chunksSent: 0}, 14 | 'bufferLength': 0, 15 | 'bandwidth': 0, 16 | } 17 | this.bwHistory = [] 18 | } 19 | 20 | setMain(main) { 21 | this.main = main 22 | this.triggerStats({status: "on", occupiedSlots: 0, totalSlots: Settings.maxUploadSlots}) 23 | this.updateData({delay: this.main.el.getDelay()}) 24 | this.data.delay = this.main.el.getDelay() 25 | this.addEventListeners() 26 | } 27 | 28 | updateData(metrics) { 29 | this.triggerStats(metrics) 30 | this.data = _.extend(this.data, metrics) 31 | } 32 | 33 | timeoutFor(command) { 34 | var segmentSize = this.data.segmentSize? this.data.segmentSize * 1000: 2000 35 | if (command === 'interested') { 36 | var timeout = segmentSize / 3 37 | return timeout > 2000? 2000: timeout 38 | } else if (command === 'request') { 39 | if (this.data.lastDownloadType === 'p2p') { 40 | return segmentSize 41 | } else { 42 | return segmentSize * 0.6 43 | } 44 | } 45 | } 46 | 47 | addEventListeners() { 48 | this.listenTo(this.main.resourceRequester.p2pManager.swarm, "swarm:sizeupdate", (event) => this.updateData(event)) 49 | this.listenTo(this.main.resourceRequester.cdnRequester, 'cdnrequester:downloadtime', (event) => this.updateBandwidth(event)) 50 | this.listenTo(this.main.uploadHandler, 'uploadhandler:update', (event) => this.updateUploadSlots(event)) 51 | Clappr.Mediator.on(this.main.uniqueId + ':fragmentloaded', () => this.onFragmentLoaded()) 52 | } 53 | 54 | updateBandwidth(event) { 55 | if (!this.data.currentBitrate || !this.data.segmentSize) return 56 | var currentBw = this.data.currentBitrate * this.data.segmentSize / (event.downloadTime/1000) 57 | // nearest rank method, 80th percentile (#101) 58 | this.data.bandwidth = this.calculateBandwidth(currentBw) 59 | } 60 | 61 | calculateBandwidth(currentBw) { 62 | this.updateBwHistory(currentBw) 63 | var sortedBwHistory = this.bwHistory 64 | sortedBwHistory.sort(function(a,b) { return a-b }) 65 | var position = Math.round(0.8 * sortedBwHistory.length) 66 | return sortedBwHistory[position] || sortedBwHistory[0] 67 | } 68 | 69 | updateBwHistory(currentBw) { 70 | this.bwHistory.push(currentBw) 71 | if (this.bwHistory.length > 10) { 72 | this.bwHistory = _.rest(this.bwHistory) 73 | } 74 | } 75 | 76 | onFragmentLoaded() { 77 | var bitrate = Math.floor(this.main.getCurrentBitrate() / 1000) 78 | var bufferLength = this.main.el.globoGetbufferLength() 79 | bitrate = !_.isNaN(bitrate) ? bitrate : 'UNKNOWN' 80 | bufferLength = !_.isNaN(bufferLength) ? bufferLength: 0 81 | var data = { 82 | state: this.main.currentState, 83 | currentBitrate: bitrate, 84 | bufferLength: bufferLength.toFixed(2), 85 | segmentSize: this.getAverageSegmentSize(), 86 | levels: this.main.getLevels() 87 | } 88 | this.updateData(data) 89 | } 90 | 91 | updateChunkStats(method=null) { 92 | if (method === "p2p") { 93 | this.data.chunks.chunksFromP2P++ 94 | this.data.lastDownloadType = "p2p" 95 | } else if (method === "cdn") { 96 | this.data.chunks.chunksFromCDN++ 97 | this.data.lastDownloadType = "cdn" 98 | } else if (method === "p2psent") { 99 | this.data.chunks.chunksSent++ 100 | } 101 | this.triggerStats(this.data.chunks) 102 | } 103 | 104 | updateUploadSlots(metrics) { 105 | this.data.uploadSlots = metrics 106 | this.triggerStats(metrics) 107 | } 108 | 109 | triggerStats(metrics) { 110 | this.main.trigger('playback:stats:add', metrics) 111 | } 112 | 113 | getAverageSegmentSize() { 114 | if (!this.avgSegmentSize || this.avgSegmentSize === 0 && this.main.getLevels().length > 0) { 115 | this.avgSegmentSize = Math.round(this.main.getLevels()[0].averageduration) || 0 116 | } 117 | return this.avgSegmentSize 118 | } 119 | 120 | } 121 | 122 | PlaybackInfo.getInstance = function() { 123 | if (this._instance === undefined) { 124 | this._instance = new this() 125 | } 126 | return this._instance 127 | } 128 | 129 | module.exports = PlaybackInfo 130 | -------------------------------------------------------------------------------- /src/resource_requester.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object') 7 | var CDNRequester = require('./cdn_requester') 8 | var P2PManager = require('./p2p_manager') 9 | var Settings = require('./settings') 10 | var _ = require('underscore') 11 | 12 | class ResourceRequester extends BaseObject { 13 | constructor(params) { 14 | this.cdnRequester = new CDNRequester() 15 | this.p2pManager = new P2PManager(params) 16 | this.isInitialBuffer = true 17 | this.decodingError = false 18 | this.lowBuffer = Settings.lowBufferLength 19 | this.onDVR = false 20 | } 21 | 22 | requestResource(resource, bufferLength, callback) { 23 | this.resource = resource 24 | this.callback = callback 25 | if (this.avoidP2P() || bufferLength < this.lowBuffer || _.size(this.p2pManager.swarm.utils.contributors) === 0) { 26 | this.requestToCDN() 27 | } else { 28 | this.requestToP2P() 29 | } 30 | } 31 | 32 | avoidP2P() { 33 | return _.some([this.onDVR, this.decodingError, this.isInitialBuffer]) 34 | } 35 | 36 | requestToCDN() { 37 | this.cdnRequester.requestResource(this.resource, this.callback) 38 | } 39 | 40 | requestToP2P() { 41 | this.p2pManager.requestResource(this.resource, this.callback, this.requestToCDN.bind(this)) 42 | } 43 | } 44 | 45 | module.exports = ResourceRequester; 46 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | module.exports = { 7 | /* logging 8 | Turn on/off logging on browser's console on 9 | initialization. You can always turn on/off 10 | by pressing ctrl+shift+l on your browser tab. */ 11 | logging: false, 12 | 13 | /* maxStorageChunks 14 | Maximum size of the storage in number of chunks. */ 15 | maxStorageChunks: 6, 16 | 17 | /* maxContributors 18 | The maximum number of contributors one peer can handle. 19 | Contributors are used to be requested for video segments. */ 20 | maxContributors: 10, 21 | 22 | /* maxSwarmSize 23 | Maximum number of peers on our particular swarm view. 24 | When reach this number, P2PManager will ignore new peers. */ 25 | maxSwarmSize: 100, 26 | 27 | /* maxUploadSlots 28 | Maximum number of peers one can serve. */ 29 | maxUploadSlots: 10, 30 | 31 | /* uploadSlotTimeout 32 | Time in milliseconds in which an upload slot will be expired. 33 | If a given downloader stops to request segments for 34 | uploadSlotTimeout seconds, this slot will be emptied. */ 35 | uploadSlotTimeout: 8000, 36 | 37 | /* tracker 38 | Place where your rtc-switchboard server is running */ 39 | tracker: 'http://tracker.bem.tv', 40 | 41 | /* lowBufferLength 42 | Local buffer threshold in seconds in which the player 43 | will try to use P2P. Smaller than lowBufferLength, player will 44 | request chunks only for CDN. */ 45 | lowBufferLength: 5, 46 | 47 | /* points 48 | How many points a contributor win/loss when send a segment 49 | or not. This serves to reorganize peers and promoting or 50 | demoting then as contributors. */ 51 | points: 1, 52 | 53 | /* stunServers 54 | STUN servers used to match peers. */ 55 | stunServers: [ 56 | {"url": "stun:stun.bem.tv:3478"} 57 | ], 58 | 59 | /* forceAllowCredentials 60 | Some HLS servers use cookies to authenticate and create user 61 | sessions. This option enables BemTV to make requests to CDN 62 | using the cookies received when getting the master playlist. */ 63 | forceAllowCredentials: false 64 | } 65 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var Settings = require("./settings") 7 | var _ = require("underscore") 8 | var log = require('./log').getInstance() 9 | 10 | 11 | class Storage { 12 | constructor() { 13 | this.keys = [] 14 | this.chunks = {} 15 | this.CHUNK_REGEX = /(.*.ts|.*.aac).*?/ 16 | } 17 | 18 | setItem(key, value) { 19 | var normalizedKey = key.match(this.CHUNK_REGEX)[1] 20 | if (_.has(this.chunks, normalizedKey)) { 21 | log.warn("already have this chunk on storage: " + normalizedKey) 22 | } else { 23 | this.keys.push(normalizedKey) 24 | this.chunks[normalizedKey] = value 25 | this.updateSize() 26 | } 27 | } 28 | 29 | get size() { 30 | return this.keys.length 31 | } 32 | 33 | updateSize() { 34 | if (this.size > Settings.maxStorageChunks) { 35 | this.removeOlderItem() 36 | } 37 | } 38 | 39 | removeOlderItem() { 40 | var key = this.keys.splice(0, 1)[0] 41 | delete this.chunks[key] 42 | } 43 | 44 | getItem(key) { 45 | var normalizedKey = key.match(this.CHUNK_REGEX)[1] 46 | return this.chunks[normalizedKey] 47 | } 48 | 49 | contain(key) { 50 | var normalizedKey = key.match(this.CHUNK_REGEX)[1] 51 | return _.contains(this.keys, normalizedKey) 52 | } 53 | } 54 | 55 | Storage.getInstance = function() { 56 | if (this._instance === undefined) { 57 | this._instance = new this(); 58 | } 59 | return this._instance; 60 | } 61 | 62 | module.exports = Storage 63 | 64 | -------------------------------------------------------------------------------- /src/styler.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a Apache 3 | // license that can be found in the LICENSE file. 4 | 5 | var $ = require('zepto'); 6 | var _ = require('underscore'); 7 | var JST = require('./jst'); 8 | 9 | var Styler = { 10 | getStyleFor: function(name, options) { 11 | options = options || {}; 12 | return $('').html(_.template(JST.CSS[name])(options)); 13 | } 14 | }; 15 | 16 | module.exports = Styler; 17 | -------------------------------------------------------------------------------- /src/swarm.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object') 7 | var BufferedChannel = require('rtc-bufferedchannel') 8 | var Peer = require('./peer') 9 | var Settings = require('./settings') 10 | var _ = require('underscore') 11 | var log = require('./log').getInstance() 12 | var SwarmUtils = require('./swarm_utils') 13 | var PlaybackInfo = require('./playback_info') 14 | 15 | class Swarm extends BaseObject { 16 | constructor() { 17 | this.playbackInfo = PlaybackInfo.getInstance() 18 | this.utils = new SwarmUtils(this) 19 | this.peers = [] 20 | this.sender = undefined 21 | this.satisfyCandidates = [] 22 | this.chokedClients = 0 23 | } 24 | 25 | size() { 26 | return _.size(this.peers) 27 | } 28 | 29 | addPeer(id, dataChannel) { 30 | var bufferedChannel = BufferedChannel(dataChannel, {calcCharSize: false}) 31 | var peer = new Peer({ident: id, dataChannel: bufferedChannel, swarm: this}) 32 | this.peers.push(peer) 33 | this.trigger('swarm:sizeupdate', {swarmSize: this.size()}) 34 | } 35 | 36 | removePeer(id) { 37 | var peer = this.utils.findPeer(id) 38 | this.peers = _.without(this.peers, peer) 39 | log.info("quit: " + id + " (remains: " + this.size() + ")") 40 | this.trigger('swarm:sizeupdate', {swarmSize: this.size()}) 41 | } 42 | 43 | updatePeersScore() { 44 | var successPeer = this.utils.findPeer(this.sender) 45 | var goodPeers = _.union([successPeer], this.satisfyCandidates) 46 | var badPeers = _.difference(this.utils.contributors, goodPeers) 47 | this.utils.incrementScore(goodPeers) 48 | this.utils.incrementScore([successPeer]) //double sender score gain :) 49 | this.utils.decrementScore(badPeers) 50 | } 51 | 52 | sendTo(recipients, command, resource, content='') { 53 | if (recipients === 'contributors') { 54 | _.each(this.utils.contributors, function(peer) { peer.send(command, resource, content) }, this) 55 | } else { 56 | var peer = this.utils.findPeer(recipients) 57 | peer.send(command, resource, content); 58 | } 59 | } 60 | 61 | sendInterested(resource, callbackSuccess, callbackFail) { 62 | this.externalCallbackFail = callbackFail 63 | this.externalCallbackSuccess = callbackSuccess 64 | this.currentResource = resource 65 | var timeout = this.playbackInfo.timeoutFor('interested') 66 | if (this.sender) { 67 | //already have a sender with success, requesting directly 68 | this.interestedTimeoutID = setTimeout(this.sendRequest.bind(this), timeout + 1000) 69 | } else { 70 | this.sendTo('contributors', 'interested', resource) 71 | this.interestedTimeoutID = setTimeout(this.interestedFinished.bind(this), timeout) 72 | } 73 | } 74 | 75 | interestedFinished() { 76 | if (_.size(this.satisfyCandidates) > 0) { 77 | this.sender = this.utils.electSender(this.satisfyCandidates).ident 78 | log.info("round finished, candidates: " + _.size(this.satisfyCandidates) + ', selected: ' + this.sender) 79 | this.sendRequest() 80 | } else { 81 | log.info("round finished, no candidates.") 82 | this.callbackFail() 83 | } 84 | } 85 | 86 | sendRequest() { 87 | var timeout = this.playbackInfo.timeoutFor('request') 88 | this.requestFailID = setTimeout(this.callbackFail.bind(this), timeout) 89 | this.sendTo(this.sender, 'request', this.currentResource) 90 | } 91 | 92 | chokeReceived(resource) { 93 | if (this.currentResource === resource) { 94 | this.chokedClients += 1 95 | } 96 | if (this.chokedClients === _.size(this.utils.contributors) || this.sender !== undefined) { 97 | log.warn("Choked, getting from CDN") 98 | clearInterval(this.interestedTimeoutID) 99 | this.clearRequestFailInterval() 100 | this.callbackFail() 101 | } 102 | } 103 | 104 | containReceived(peer, resource) { 105 | if (this.currentResource === resource) { 106 | this.satisfyCandidates.push(peer) 107 | } 108 | } 109 | 110 | satisfyReceived(peer, resource, chunk) { 111 | if (this.sender === peer.ident && this.currentResource === resource) { 112 | this.externalCallbackSuccess(chunk, "p2p") 113 | peer.late = 0 114 | this.clearRequestFailInterval() 115 | this.updatePeersScore() 116 | this.rebootRoundVars() 117 | } else { 118 | // nothing could be worse than this. Someont sent you the entire chunk, but missed the time 119 | // and generated unnecessary traffic. 120 | peer.late += 1 121 | log.warn("satisfy error due timeout") 122 | if (peer.late > 3) { 123 | this.busyReceived(peer) 124 | peer.late = 0 125 | } 126 | } 127 | } 128 | 129 | busyReceived(peer) { 130 | var lowerScore = this.utils.getLowestScorePeer().score 131 | peer.score = lowerScore - Settings.points 132 | log.warn(peer.ident + " score is now: " + peer.score) 133 | } 134 | 135 | callbackFail() { 136 | this.utils.decrementScore(this.utils.contributors) 137 | this.rebootRoundVars() 138 | this.sender = undefined 139 | this.externalCallbackFail() 140 | } 141 | 142 | rebootRoundVars() { 143 | this.currentResource = undefined 144 | this.chokedClients = 0 145 | this.satisfyCandidates = [] 146 | this.trigger('swarm:sizeupdate', {swarmSize: this.size()}) 147 | } 148 | 149 | clearRequestFailInterval() { 150 | clearInterval(this.requestFailID) 151 | this.requestFailID = 0 152 | } 153 | } 154 | 155 | module.exports = Swarm 156 | -------------------------------------------------------------------------------- /src/swarm_utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object') 7 | var _ = require('underscore') 8 | var Settings = require('./settings') 9 | 10 | class SwarmUtils extends BaseObject { 11 | constructor(swarm) { 12 | this.swarm = swarm 13 | } 14 | 15 | findPeer(id) { 16 | return _.find(this.swarm.peers, function (peer) { 17 | return (peer.ident === id) 18 | }, this) 19 | } 20 | 21 | incrementScore(peers) { 22 | this.changeScore(peers, Settings.points) 23 | } 24 | 25 | decrementScore(peers) { 26 | this.changeScore(peers, Settings.points * -1) 27 | } 28 | 29 | changeScore(peers, points) { 30 | _.each(peers, function(peer) { peer.score += points }, this) 31 | } 32 | 33 | electSender(candidates) { 34 | return _.first(_.sortBy(candidates, function (p) { return p.score }).reverse()) 35 | } 36 | 37 | get contributors() { 38 | var activePeers = _.filter(this.swarm.peers, function (p) { return p.active }) 39 | var orderedPeers = _.sortBy(activePeers, function (p) { return p.score }).reverse() 40 | if (_.size(this.swarm.peers) > Settings.maxContributors) { 41 | var slice = orderedPeers.slice(0, Settings.maxContributors) 42 | return slice 43 | } else { 44 | return orderedPeers 45 | } 46 | } 47 | 48 | getLowestScorePeer() { 49 | return _.first(_.sortBy(this.swarm.peers, function(p) { return p.score })) 50 | } 51 | } 52 | 53 | module.exports = SwarmUtils 54 | -------------------------------------------------------------------------------- /src/upload_handler.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Flávio Ribeiro . 2 | // All rights reserved. 3 | // Use of this source code is governed by a Apache 4 | // license that can be found in the LICENSE file. 5 | 6 | var BaseObject = require('base_object') 7 | var Settings = require("./settings") 8 | var _ = require("underscore") 9 | var log = require('./log').getInstance() 10 | 11 | class UploadHandler extends BaseObject { 12 | constructor() { 13 | this.maxUploadSlots = Settings.maxUploadSlots 14 | this.slots = {} 15 | setInterval(this.checkAndFreeSlots.bind(this), 5000) 16 | } 17 | 18 | getSlot(peerId) { 19 | if (_.contains(_.keys(this.slots), peerId) || this.hasFreeSlots()) { 20 | this.slots[peerId] = Date.now() 21 | this.updateSlotsCount() 22 | return true 23 | } else { 24 | log.warn("don't have free upload slots") 25 | return false 26 | } 27 | } 28 | 29 | checkAndFreeSlots() { 30 | var threshold = Date.now() - Settings.uploadSlotTimeout 31 | _.each(this.slots, function (timestamp, peerId) { 32 | if (timestamp <= threshold) { 33 | delete this.slots[peerId] 34 | this.updateSlotsCount() 35 | } 36 | }, this) 37 | } 38 | 39 | hasFreeSlots() { 40 | return (_.size(this.slots) < this.maxUploadSlots) 41 | } 42 | 43 | updateSlotsCount() { 44 | this.trigger('uploadhandler:update', {occupiedSlots: _.size(this.slots), totalSlots: this.maxUploadSlots}) 45 | } 46 | } 47 | 48 | UploadHandler.getInstance = function() { 49 | if (this._instance === undefined) { 50 | this._instance = new this() 51 | } 52 | return this._instance 53 | } 54 | 55 | module.exports = UploadHandler 56 | -------------------------------------------------------------------------------- /test/flash_uploader_spec.js: -------------------------------------------------------------------------------- 1 | var FlashUploader = require('../src/flash_uploader'); 2 | 3 | describe('FlashUploader', function() { 4 | beforeEach(() => { 5 | this.receivedChunk = ''; 6 | this.clock = sinon.useFakeTimers(Date.now()); 7 | this.sendPartCallback = function(subSegment) { this.receivedChunk += subSegment }.bind(this); 8 | this.finishCallback = function() {}.bind(this); 9 | this.flashUploader = new FlashUploader(); 10 | this.flashUploader.MAX_SIZE = 10; 11 | }) 12 | 13 | it('should send chunks smaller than MAX_SIZE', () => { 14 | this.chunk = (new Array(this.flashUploader.MAX_SIZE - 1).join("x")); 15 | this.flashUploader.send(this.sendPartCallback, this.chunk, this.finishCallback); 16 | this.clock.tick(100); 17 | expect(this.receivedChunk).to.be.equal(this.chunk); 18 | }); 19 | 20 | it('should send chunks bigger than MAX_SIZE', () => { 21 | this.chunk = (new Array(this.flashUploader.MAX_SIZE * 50).join("x")); 22 | this.chunk += "xxx"; 23 | this.flashUploader.send(this.sendPartCallback, this.chunk, this.finishCallback); 24 | this.clock.tick(100); 25 | expect(this.receivedChunk).to.be.equal(this.chunk); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/storage_spec.js: -------------------------------------------------------------------------------- 1 | var Settings = require('../src/settings'); 2 | var Storage = require('../src/storage') 3 | 4 | describe('Storage', function() { 5 | beforeEach(() => { 6 | this.storage = new Storage(); 7 | Settings.maxStorageChunks = 10; 8 | }) 9 | 10 | it('should store segment', () => { 11 | this.storage.setItem("segmentName.ts", "segmentContent"); 12 | expect(this.storage.contain("segmentName.ts")).to.be.true; 13 | expect(this.storage.getItem("segmentName.ts")).to.be.equal('segmentContent'); 14 | }); 15 | 16 | it('should not store equal segments', () => { 17 | this.storage.setItem("segmentName.ts", "segmentContent"); 18 | this.storage.setItem("segmentName.ts", "segmentContent"); 19 | expect(this.storage.size).to.be.equal(1); 20 | }); 21 | 22 | it('should respect maxStorageChunks', () => { 23 | Settings.maxStorageChunks = 3 24 | this.storage.setItem("segment1.ts", "segmentContent1"); 25 | this.storage.setItem("segment2.ts", "segmentContent2"); 26 | this.storage.setItem("segment3.ts", "segmentContent3"); 27 | this.storage.setItem("segment4.ts", "segmentContent4"); 28 | this.storage.setItem("segment5.ts", "segmentContent5"); 29 | expect(this.storage.size).to.be.equal(3); 30 | }); 31 | 32 | it('should remove older chunks when reach maxStorageChunks', () => { 33 | Settings.maxStorageChunks = 3 34 | this.storage.setItem("segment1.ts", "segmentContent1"); 35 | this.storage.setItem("segment2.ts", "segmentContent2"); 36 | this.storage.setItem("segment3.ts", "segmentContent3"); 37 | this.storage.setItem("segment4.ts", "segmentContent4"); 38 | expect(this.storage.contain("segment1.ts")).to.be.false; 39 | }); 40 | 41 | it('should discard querystrings', () => { 42 | Settings.maxStorageChunks = 3 43 | this.storage.setItem("segment1.ts?query=string", "segmentContent1"); 44 | expect(this.storage.contain("segment1.ts")).to.be.true; 45 | expect(this.storage.contain("segment1.ts?query=string")).to.be.true; 46 | }); 47 | 48 | it('should consider audio only segments', () => { 49 | Settings.maxStorageChunks = 3 50 | this.storage.setItem("segment1.aac?query=string", "segmentContent1"); 51 | expect(this.storage.contain("segment1.aac")).to.be.true; 52 | expect(this.storage.contain("segment1.aac?query=string")).to.be.true; 53 | }); 54 | 55 | 56 | it('should store big segments', () => { 57 | var bigSegment = (new Array(10*1024*1024)).join("x"); 58 | for (var i = 0; i <= Settings.maxStorageChunks; i++) { 59 | this.storage.setItem("segment" + i + ".ts", bigSegment); 60 | } 61 | expect(this.storage.getItem('segment1.ts')).to.be.equal(bigSegment); 62 | }); 63 | 64 | }); 65 | --------------------------------------------------------------------------------