├── .gitignore ├── sample ├── birds.ogg ├── index.html └── app.js ├── package.json ├── Changelog.md ├── LICENSE ├── Readme.md ├── libs └── exts.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /sample/birds.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obastemur/mediaserver/HEAD/sample/birds.ogg -------------------------------------------------------------------------------- /sample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | ms = require('../index'); 3 | 4 | var path = require('path'); 5 | 6 | http.createServer(function (req, res) { 7 | 8 | var _path; 9 | if(req.url == "/" || req.url == "/index.html"){ 10 | _path = "/index.html"; 11 | } else if (req.url == "/birds.ogg") { 12 | _path = "/birds.ogg"; 13 | } else { 14 | res.write("Target Not Found!" ); 15 | res.end(); 16 | return; 17 | } 18 | 19 | ms.pipe(req, res, path.join(__dirname, _path), path.extname(_path)); 20 | 21 | }).listen(1337, '127.0.0.1'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediaserver", 3 | "description": "Media & static asset streaming for http(s) server", 4 | "version": "0.1.1", 5 | "author": { 6 | "name": "Oguz Bastemur", 7 | "email": "obastemur@gmail.com" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/obastemur/mediaserver" 12 | }, 13 | "engines": { 14 | "node": ">= 0.10.0" 15 | }, 16 | "license": "MIT", 17 | "homepage": "https://github.com/obastemur/mediaserver", 18 | "readmeFilename": "Readme.md" 19 | } 20 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 0.1.1 2 | * clean up the file stream on response end/close/finish (#10) 3 | 4 | 0.1.0 5 | * Desktop safari media streaming fix 6 | 7 | 0.0.9 8 | * minor stability improvements 9 | 10 | 0.0.8 11 | * don't cache on'noCache 12 | 13 | 0.0.7 14 | * consistent mime type from extension 15 | * mp4 added to exts list 16 | 17 | 0.0.6 18 | * code style update 19 | * minor improvements and stability updates 20 | * streaming fix for chrome 21 | 22 | 0.0.4 - 0.0.5 23 | * event support for extension streaming 24 | 25 | 0.0.3 26 | 27 | * exports.mediaTypes proxies the media types 28 | * mediaserver now serves other file types (html, css, js etc) 29 | * cross domain headers added 30 | * pipe returns true/false based on the success (do not close the response if it wasn't successful) 31 | 32 | 0.0.2 33 | 34 | * direct flow support 35 | * cross browser tests made 36 | 37 | 38 | 0.0.1 39 | 40 | * initial version 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Oguz Bastemur 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 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | #### Media & static asset streaming module for node.js http(s) server 2 | 3 | #### Compatibility 4 | (Tested on IE 6+, FF, Chrome, Mobile Safari - IE/Edge - Chrome and Brave) 5 | 6 | #### Installation 7 | ```npm install mediaserver``` 8 | 9 | #### Application Sample 10 | Visit `sample` folder for sample application. 11 | 12 | #### Sample Usage 13 | ``` 14 | var http = require('http'), 15 | ms = require('mediaserver'); 16 | 17 | http.createServer(function (req, res) { 18 | 19 | ms.pipe(req, res, "music.mp3"); 20 | 21 | }).listen(1337, '127.0.0.1'); 22 | ``` 23 | 24 | from the client side 25 | 26 | ``` 27 | 31 | ``` 32 | 33 | #### express 34 | ``` 35 | app.get('/music.mp3', function(req, res){ 36 | ms.pipe(req, res, "/music.mp3"); 37 | }); 38 | ``` 39 | 40 | #### API 41 | 42 | `.noCache` (true/false) enable/disable caching `file stat` results (default enabled) 43 | 44 | `.mediaTypes` Dictionary of media types and their corresponding media identifiers (i.e. "mp4" => "video/mpeg") 45 | 46 | `.pipe(request, response, path, extension or media identifier)` pipe a file from file system to browser 47 | 48 | 49 | #### LICENSE 50 | MIT 51 | -------------------------------------------------------------------------------- /libs/exts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mediaserver module for JXcore and Node.JS 3 | * 4 | * MIT license, Oguz Bastemur 2014 5 | * 6 | * 7 | * media extensions 8 | */ 9 | 10 | // ref : http://www.webmaster-toolkit.com/mime-types.shtml 11 | exports = module.exports = { 12 | 13 | // plain formats 14 | ".html": "text/html", 15 | ".css": "text/css", 16 | ".js": "text/javascript", 17 | ".txt": "text/plain", 18 | 19 | // custom 20 | ".pdf": "application/octet-stream", 21 | ".woff": "application/octet-stream", 22 | ".ttf": "application/octet-stream", 23 | ".svg": "application/octet-stream", 24 | ".otf": "application/octet-stream", 25 | ".eot": "application/octet-stream", 26 | 27 | // compressed formats 28 | ".zip": "application/octet-stream", 29 | ".rar": "application/octet-stream", 30 | ".7z": "application/octet-stream", 31 | ".gz": "application/octet-stream", 32 | ".tar": "application/octet-stream", 33 | 34 | // media formats 35 | ".afl": "video/animaflex", 36 | ".ai": "application/postscript", 37 | ".aif": "audio/aiff", 38 | ".aifc": "audio/aiff", 39 | ".aiff": "audio/aiff", 40 | ".aip": "text/x-audiosoft-intra", 41 | ".art": "image/x-jg", 42 | ".asf": "video/x-ms-asf", 43 | ".asm": "text/x-asm", 44 | ".asx": "video/x-ms-asf", 45 | ".au": "audio/basic", 46 | ".avi": "video/avi", 47 | ".avs": "video/avs-video", 48 | ".bm": "image/bmp", 49 | ".bmp": "image/bmp", 50 | ".dif": "video/x-dv", 51 | ".dl": "video/dl", 52 | ".dv": "video/x-dv", 53 | ".dwg": "image/vnd.dwg", 54 | ".dxf": "image/vnd.dwg", 55 | ".fli": "video/fli", 56 | ".flo": "image/florian", 57 | ".flac": "audio/flac", 58 | ".fmf": "video/x-atomic3d-feature", 59 | ".fpx": "image/vnd.fpx", 60 | ".funk": "audio/make", 61 | ".g3": "image/g3fax", 62 | ".gif": "image/gif", 63 | ".gl": "video/gl", 64 | ".gsd": "audio/x-gsm", 65 | ".gsm": "audio/x-gsm", 66 | ".isu": "video/x-isvideo", 67 | ".it": "audio/it", 68 | ".jam": "audio/x-jam", 69 | ".jfif": "image/jpeg", 70 | ".jfif-tbnl": "image/jpeg", 71 | ".jpe": "image/jpeg", 72 | ".jpeg": "image/jpeg", 73 | ".jpg": "image/jpeg", 74 | ".jps": "image/x-jps", 75 | ".jut": "image/jutvision", 76 | ".kar": "audio/midi", 77 | ".la": "audio/nspaudio", 78 | ".lam": "audio/x-liveaudio", 79 | ".lma": "audio/x-nspaudio", 80 | ".m1v": "video/mpeg", 81 | ".m2a": "audio/mpeg", 82 | ".m2v": "video/mpeg", 83 | ".m3u": "audio/x-mpequrl", 84 | ".mcf": "image/vasa", 85 | ".mid": "audio/midi", 86 | ".midi": "audio/midi", 87 | ".mjf": "audio/x-vnd.audioexplosion.mjuicemediafile", 88 | ".mjpg": "video/x-motion-jpeg", 89 | ".mod": "audio/mod", 90 | ".moov": "video/quicktime", 91 | ".mov": "video/quicktime", 92 | ".movie": "video/x-sgi-movie", 93 | ".mp2": "video/mpeg", 94 | ".mp3": "audio/mpeg", 95 | ".mpa": "audio/mpeg", 96 | ".mpe": "video/mpeg", 97 | ".mpeg": "video/mpeg", 98 | ".mp4": "video/mpeg", 99 | ".mpg": "video/mpeg", 100 | ".mpga": "audio/mpeg", 101 | ".mv": "video/x-sgi-movie", 102 | ".my": "audio/make", 103 | ".nap": "image/naplps", 104 | ".naplps": "image/naplps", 105 | ".nif": "image/x-niff", 106 | ".niff": "image/x-niff", 107 | ".ogg": "audio/ogg", 108 | ".pbm": "image/x-portable-bitmap", 109 | ".pct": "image/x-pict", 110 | ".pcx": "image/x-pcx", 111 | ".pfunk": "audio/make", 112 | ".pgm": "image/x-portable-greymap", 113 | ".pic": "image/pict", 114 | ".pict": "image/pict", 115 | ".pm": "image/x-xpixmap", 116 | ".png": "image/png", 117 | ".pnm": "image/x-portable-anymap", 118 | ".ppm": "image/x-portable-pixmap", 119 | ".qcp": "audio/vnd.qcelp", 120 | ".qif": "image/x-quicktime", 121 | ".qt": "video/quicktime", 122 | ".qtc": "video/x-qtc", 123 | ".qti": "image/x-quicktime", 124 | ".qtif": "image/x-quicktime", 125 | ".ra": "audio/x-realaudio", 126 | ".ram": "audio/x-pn-realaudio", 127 | ".ras": "image/cmu-raster", 128 | ".rast": "image/cmu-raster", 129 | ".rf": "image/vnd.rn-realflash", 130 | ".rgb": "image/x-rgb", 131 | ".rm": "audio/x-pn-realaudio", 132 | ".rmi": "audio/mid", 133 | ".rmm": "audio/x-pn-realaudio", 134 | ".rmp": "audio/x-pn-realaudio", 135 | ".rp": "image/vnd.rn-realpix", 136 | ".rpm": "audio/x-pn-realaudio-plugin", 137 | ".rv": "video/vnd.rn-realvideo", 138 | ".s3m": "audio/s3m", 139 | ".scm": "video/x-scm", 140 | ".sid": "audio/x-psid", 141 | ".snd": "audio/x-adpcm", 142 | ".svf": "image/vnd.dwg", 143 | ".tif": "image/tiff", 144 | ".tiff": "image/tiff", 145 | ".tsi": "audio/tsp-audio", 146 | ".tsp": "audio/tsplayer", 147 | ".vdo": "video/vdo", 148 | ".viv": "video/vivo", 149 | ".vivo": "video/vivo", 150 | ".voc": "audio/voc", 151 | ".vos": "video/vosaic", 152 | ".vox": "audio/voxware", 153 | ".vqe": "audio/x-twinvq-plugin", 154 | ".vqf": "audio/x-twinvq", 155 | ".vql": "audio/x-twinvq-plugin", 156 | ".wav": "audio/wav", 157 | ".wbmp": "image/vnd.wap.wbmp", 158 | ".xbm": "image/xbm", 159 | ".xdr": "video/x-amt-demorun", 160 | ".xif": "image/vnd.xiff", 161 | ".xm": "audio/xm", 162 | ".xmz": "xgl/movie", 163 | ".xpm": "image/xpm", 164 | ".x-png": "image/png", 165 | ".xsr": "video/x-amt-showrun" 166 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mediaserver module for node.js 3 | * 4 | * MIT license, Oguz Bastemur 2014-2018 5 | */ 6 | 7 | var fs = require('fs'), 8 | exts = require('./libs/exts'), 9 | pathModule = require('path'); 10 | 11 | var pipe_extensions = {}; 12 | var pipe_extension_id = 0; 13 | 14 | var shared = {}; 15 | var fileInfo = function (path) { 16 | if (path) { 17 | if (!exports.noCache && shared[path]) { 18 | return shared[path]; 19 | } 20 | else { 21 | if (!fs.existsSync(path)) { 22 | return null; 23 | } 24 | var stat = fs.statSync(path); 25 | if (!exports.noCache) 26 | shared[path] = stat.size; 27 | 28 | return stat.size; 29 | } 30 | } 31 | return 0; 32 | }; 33 | 34 | // set this to true for development mode 35 | exports.noCache = false; 36 | exports.mediaTypes = exts; 37 | 38 | var getRange = function (req, total) { 39 | var range = [0, total, 0]; 40 | var rinfo = req.headers ? req.headers.range : null; 41 | 42 | if (rinfo) { 43 | var rloc = rinfo.indexOf('bytes='); 44 | if (rloc >= 0) { 45 | var ranges = rinfo.substr(rloc + 6).split('-'); 46 | try { 47 | range[0] = parseInt(ranges[0]); 48 | if (ranges[1] && ranges[1].length) { 49 | range[1] = parseInt(ranges[1]); 50 | range[1] = range[1] < 16 ? 16 : range[1]; 51 | } 52 | } catch (e) {} 53 | } 54 | 55 | if (range[1] == total) 56 | range[1]--; 57 | 58 | range[2] = total; 59 | } 60 | 61 | return range; 62 | }; 63 | 64 | 65 | var isString = function (str) { 66 | if (!str) return false; 67 | return (typeof str == 'string' || str instanceof String); 68 | }; 69 | 70 | 71 | exports.pipe = function (req, res, path, type, opt_cb) { 72 | if (!isString(path)) { 73 | throw new TypeError("path must be a string value"); 74 | } 75 | 76 | var total = fileInfo(path); 77 | 78 | if (total == null) { 79 | res.end(path + " not found"); 80 | return false; 81 | } 82 | 83 | var range = getRange(req, total); 84 | 85 | var ext = pathModule.extname(path).toLowerCase(); 86 | if (!type && ext && ext.length) { 87 | type = exts[ext]; 88 | } 89 | 90 | if (type && type.length && type[0] == '.') { 91 | ext = type; 92 | type = exts[type]; 93 | } 94 | 95 | if (!type || !type.length) { 96 | res.write("Media format not found for " + pathModule.basename(path)); 97 | } else { 98 | var file = fs.createReadStream(path, {start: range[0], end: range[1]}); 99 | 100 | var cleanupFileStream = function() { 101 | file.close(); 102 | } 103 | 104 | // the event emitted seems to change based on version of node.js 105 | // 'close' is fired as of v6.11.5 106 | res.on('close', cleanupFileStream); // https://stackoverflow.com/a/9021242 107 | res.on('end', cleanupFileStream); // https://stackoverflow.com/a/16897986 108 | res.on('finish', cleanupFileStream); // https://stackoverflow.com/a/14093091 - https://stackoverflow.com/a/38057516 109 | 110 | if (!ext.length || !pipe_extensions[ext]) { 111 | var header = { 112 | 'Content-Length': range[1], 113 | 'Content-Type': type, 114 | 'Access-Control-Allow-Origin': req.headers.origin || "*", 115 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 116 | 'Access-Control-Allow-Headers': 'POST, GET, OPTIONS' 117 | }; 118 | 119 | if (range[2]) { 120 | header['Accept-Ranges'] = 'bytes'; 121 | header['Content-Range'] = 'bytes ' + range[0] + '-' + range[1] + '/' + total; 122 | header['Content-Length'] = range[2]; 123 | 124 | res.writeHead(206, header); 125 | } else { 126 | res.writeHead(200, header); 127 | } 128 | 129 | file.pipe(res); 130 | file.on('close', function () { 131 | res.end(0); 132 | if (opt_cb && typeof opt_cb == 'function') { 133 | opt_cb(path); 134 | } 135 | }); 136 | } else { 137 | var _exts = pipe_extensions[ext]; 138 | res.writeHead(200, 139 | { 140 | 'Content-Type': type, 141 | 'Access-Control-Allow-Origin': req.headers.origin || "*", 142 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 143 | 'Access-Control-Allow-Headers': 'POST, GET, OPTIONS' 144 | }); 145 | for (var o in _exts) { 146 | _exts[o](file, req, res, function () { 147 | if (!res.__ended) { 148 | res.__ended = true; 149 | res.end(0); 150 | } 151 | }); 152 | } 153 | } 154 | 155 | return true; 156 | } 157 | 158 | return false; 159 | }; 160 | 161 | exports.on = function (ext, m) { 162 | if (!pipe_extensions[ext]) { 163 | pipe_extensions[ext] = []; 164 | } 165 | 166 | m.pipe_extension_id = pipe_extension_id++; 167 | m.pipe_extension = ext; 168 | 169 | pipe_extensions[ext].push(m); 170 | }; 171 | 172 | exports.removeEvent = function (method) { 173 | if (!method || !method.pipe_extension || !method.pipe_extension_id) { 174 | return; 175 | } 176 | 177 | if (pipe_extensions[method.pipe_extension]) { 178 | var exts = pipe_extensions[method.pipe_extension]; 179 | for (var i = 0, ln = exts.length; i < ln; i++) { 180 | if (exts[i].pipe_extension_id == method.pipe_extension_id) { 181 | pipe_extensions[method.pipe_extension] = exts.splice(i, 1); 182 | } 183 | } 184 | } 185 | }; 186 | --------------------------------------------------------------------------------