├── .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 |
--------------------------------------------------------------------------------