├── .gitignore ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hls-buffer 2 | 3 | Preload and buffer http live streams (aka do not lag on crappy networks) 4 | 5 | npm install hls-buffer 6 | 7 | ## Usage 8 | 9 | hls-buffer takes a m3u8 url from a remote server and preloads and buffers the linked .ts files in memory 10 | 11 | ``` js 12 | var hls = require('hls-buffer'); 13 | var buffer = hls('http://my-favorite-stream.com/some/path/index.m3u8'); 14 | var http = require('http'); 15 | 16 | var server = http.createServer(function(request, response) { 17 | if (request.url === '/index.m3u8') { 18 | // first return a playlist 19 | buffer.playlist(function(err, pl) { 20 | response.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); 21 | response.end(pl); 22 | }); 23 | } else { 24 | // else return the linked segment 25 | var stream = buffer.segment(request.url); 26 | response.setHeader('Content-Type', 'video/mp2s'); 27 | stream.pipe(response); 28 | } 29 | }); 30 | 31 | server.listen(8080); 32 | ``` 33 | 34 | If you run the above example with your favorite http live streaming service a local preloading proxy 35 | will be started on `http://localhost:8080/index.m3u8`. 36 | 37 | Per default up to 10 segments will be buffered in ram. To change this pass `{max:number}` as the second parameter 38 | to the constructor 39 | 40 | ## License 41 | 42 | MIT 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream-wrapper'); 2 | var ForeverAgent = require('forever-agent'); 3 | var request = require('request'); 4 | var once = require('once'); 5 | var crypto = require('crypto'); 6 | var thunky = require('thunky'); 7 | var resolveUrl = require('url').resolve; 8 | 9 | var noop = function() {}; 10 | 11 | var md5 = function(str) { 12 | return crypto.createHash('md5').update(str).digest('hex'); 13 | }; 14 | 15 | module.exports = function(url, opts) { 16 | if (!opts) opts = {}; 17 | 18 | var queued = []; 19 | var that = {}; 20 | var max = opts.max || 10; // default max ~10 in queue 21 | var onplaylist; 22 | 23 | var agent = undefined; 24 | 25 | if (opts.agent !== false) agent = /^https/.test(url) ? new ForeverAgent.SSL() : new ForeverAgent(); 26 | 27 | var readPlaylist = function(playlistUrl, callback) { 28 | var ts = []; 29 | request(playlistUrl, {agent:agent}, function(err, response) { 30 | if (response.statusCode !== 200) return callback(); 31 | 32 | var body = response.body.toString().trim(); 33 | 34 | body = body.split('\n').map(function(line) { 35 | if (line[0] === '#') return line; 36 | var url = resolveUrl(playlistUrl, line); 37 | var id = '/'+md5(line)+'.ts'; 38 | ts.push({url:url, id:id}); 39 | return id; 40 | }).join('\n')+'\n'; 41 | 42 | callback(null, ts, body); 43 | }); 44 | }; 45 | 46 | var preload = function() { 47 | if (that.destroyed) return; 48 | 49 | var fetching = queued.some(function(q) { 50 | return q.source && !q.finished; 51 | }); 52 | 53 | if (fetching) return; 54 | 55 | fetching = queued.some(function(q, i) { 56 | if (q.finished || q.source) return; 57 | if (i > max) return true; 58 | 59 | var req = request(q.url, {agent:agent}); 60 | var onclose = once(function() { 61 | q.finished = true; 62 | q.streams.forEach(function(s) { 63 | s.push(null); 64 | }); 65 | preload(); 66 | }); 67 | 68 | req.on('data', function(buf) { 69 | q.buffers.push(buf); 70 | q.streams.forEach(function(s) { 71 | s.push(buf); 72 | }); 73 | }); 74 | req.on('end', onclose); 75 | req.on('close', onclose); 76 | 77 | q.source = req; 78 | 79 | return true; 80 | }); 81 | 82 | if (fetching) return; 83 | if (queued.length >= max) return; 84 | 85 | that.playlist(); // refetch the playlist so we can preload some more stuff... 86 | }; 87 | 88 | that.destroyed = false; 89 | that.playlist = function(callback) { 90 | if (!callback) callback = noop; 91 | if (onplaylist) return onplaylist(callback); 92 | 93 | onplaylist = thunky(function(callback) { 94 | var retries = 0; 95 | var retry = function() { 96 | if (that.destroyed) return callback(new Error('destroyed')); 97 | 98 | readPlaylist(url, function(err, ts, playlist) { 99 | if (!ts) return callback(new Error('could not fetch playlist')); 100 | 101 | if (err && retries < 5) { 102 | retries++; 103 | return setTimeout(retry, 5000); 104 | } 105 | 106 | if (err) return callback(err); // retry instead 107 | 108 | ts.forEach(function(item) { 109 | var exists = queued.some(function(q) { 110 | return q.url === item.url; 111 | }); 112 | 113 | item.buffers = []; 114 | item.streams = []; 115 | 116 | if (!exists) queued.push(item); 117 | }); 118 | 119 | preload(); 120 | callback(null, playlist); 121 | 122 | process.nextTick(function() { // maybe wait a bit longer to bust the old playlist? 123 | onplaylist = null; 124 | }); 125 | }); 126 | }; 127 | retry(); 128 | }); 129 | 130 | onplaylist(callback); 131 | }; 132 | 133 | that.segment = function(hex) { 134 | if (that.destroyed) return null; 135 | 136 | // gc 137 | while (queued.length) { 138 | var q = queued[0]; 139 | if (q.id === hex) break; 140 | queued.shift(); 141 | if (q.source && !q.finished) q.source.destroy(); 142 | } 143 | 144 | if (!queued[0]) return null; 145 | 146 | var rs = stream.readable(); 147 | var q = queued[0]; 148 | 149 | preload(); 150 | q.streams.push(rs); 151 | q.buffers.forEach(function(b) { 152 | rs.push(b); 153 | }); 154 | 155 | if (q.finished) rs.push(null); 156 | 157 | rs.on('close', function() { 158 | q.streams.splice(q.streams.indexOf(rs), 1); 159 | }); 160 | 161 | return rs; 162 | }; 163 | 164 | that.destroy = function() { 165 | that.destroyed = true; 166 | queued.forEach(function(q) { 167 | if (q.source && !q.finished) q.source.destroy(); 168 | }); 169 | queued = []; 170 | }; 171 | 172 | return that; 173 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hls-buffer", 3 | "description": "Preload and buffer http live streams (aka do not lag on crappy networks)", 4 | "version": "0.1.5", 5 | "repository": "git://github.com/mafintosh/hls-buffer.git", 6 | "dependencies": { 7 | "thunky": "~0.1.0", 8 | "once": "~1.2.0", 9 | "stream-wrapper": "~0.1.2", 10 | "request": "~2.27.0", 11 | "forever-agent": "~0.5.0" 12 | } 13 | } 14 | --------------------------------------------------------------------------------