├── .gitignore ├── examples ├── web │ ├── favicon.ico │ ├── css │ │ └── demo.css │ ├── index.html │ └── index.js ├── server-owned │ ├── package.json │ └── src │ │ └── index.js ├── server-shared │ ├── package.json │ └── src │ │ └── index.js └── README.md ├── .eslintrc.js ├── LICENSE ├── eslint.config.js ├── package.json ├── README.md └── src └── whep.js /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /examples/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetecho/simple-whep-server/HEAD/examples/web/favicon.ico -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "jquery": true, 5 | "node": true, 6 | "es2021": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended" 10 | ], 11 | "parserOptions": { 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /examples/server-owned/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "janus-whep-server-owned", 3 | "description": "WHEP server where the library spawns the REST backend", 4 | "type": "module", 5 | "keywords": [ 6 | "whep", 7 | "wish", 8 | "janus", 9 | "webrtc", 10 | "meetecho" 11 | ], 12 | "author": { 13 | "name": "Lorenzo Miniero", 14 | "email": "lorenzo@meetecho.com" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/meetecho/simple-whep-server.git" 19 | }, 20 | "license": "ISC", 21 | "private": true, 22 | "main": "src/index.js", 23 | "dependencies": {}, 24 | "scripts": { 25 | "build": "npm install --omit=dev", 26 | "start": "node src/index.js" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/server-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "janus-whep-server-shared", 3 | "description": "WHEP server where the library reuses an existing REST backend", 4 | "type": "module", 5 | "keywords": [ 6 | "whep", 7 | "wish", 8 | "janus", 9 | "webrtc", 10 | "meetecho" 11 | ], 12 | "author": { 13 | "name": "Lorenzo Miniero", 14 | "email": "lorenzo@meetecho.com" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/meetecho/simple-whep-server.git" 19 | }, 20 | "license": "ISC", 21 | "private": true, 22 | "main": "src/index.js", 23 | "dependencies": {}, 24 | "scripts": { 25 | "build": "npm install --omit=dev", 26 | "start": "node src/index.js" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2025, Meetecho s.r.l. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import js from '@eslint/js'; 3 | 4 | export default [ 5 | { 6 | files: [ 7 | 'src/**/*.js', 8 | 'examples/**/*.js' 9 | ], 10 | ignores: [ 11 | 'examples/web/**/*' 12 | ], 13 | languageOptions: { 14 | ecmaVersion: 'latest', 15 | sourceType: 'module', 16 | globals: { 17 | ...globals.browser, 18 | ...globals.node 19 | } 20 | }, 21 | rules: { 22 | ...js.configs.recommended.rules, 23 | 'no-unused-vars': [ 24 | 'warn', 25 | { 26 | 'args': 'all', 27 | 'vars': 'all', 28 | 'caughtErrors': 'all', 29 | 'argsIgnorePattern': '^_', 30 | 'varsIgnorePattern': '^_', 31 | 'caughtErrorsIgnorePattern': '^_' 32 | } 33 | ], 34 | 'indent': [ 35 | 'warn', 36 | 'tab', 37 | { 38 | 'SwitchCase': 1 39 | } 40 | ], 41 | 'quotes': [ 42 | 'warn', 43 | 'single' 44 | ], 45 | 'semi': [ 46 | 'warn', 47 | 'always' 48 | ], 49 | 'no-empty': 'off', 50 | 'multiline-comment-style': 0 51 | } 52 | } 53 | ]; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "janus-whep-server", 3 | "description": "Simple Janus-based WHEP server library", 4 | "version": "1.0.0", 5 | "type": "module", 6 | "keywords": [ 7 | "whep", 8 | "wish", 9 | "janus", 10 | "webrtc", 11 | "meetecho" 12 | ], 13 | "author": { 14 | "name": "Lorenzo Miniero", 15 | "email": "lorenzo@meetecho.com", 16 | "url": "https://www.meetecho.com" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/meetecho/simple-whep-server.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/meetecho/simple-whep-server/issues" 24 | }, 25 | "license": "ISC", 26 | "main": "src/whep.js", 27 | "exports": { 28 | ".": "./src/whep.js" 29 | }, 30 | "files": [ 31 | "src/whep.js" 32 | ], 33 | "dependencies": { 34 | "cors": "^2.8.5", 35 | "express": "^5.1.0", 36 | "janode": "^1.7.4" 37 | }, 38 | "devDependencies": { 39 | "@eslint/js": "^9.4.0", 40 | "eslint": "^9.4.0", 41 | "globals": "^15.4.0" 42 | }, 43 | "engines": { 44 | "node": " >=18.18.0" 45 | }, 46 | "scripts": { 47 | "build": "npm install --omit=dev", 48 | "lint": "npx eslint --debug" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Simple WHEP Server examples 2 | =========================== 3 | 4 | This folder contains a few example applications using the Janus WHEP library. 5 | 6 | * The `server-owned` folder contains an example where the application asks the library to create a new REST server to host the WHEP functionality, binding to the provided port. A separate REST server is then spawned by the application for its own purposes (e.g., listing the available endpoints). A sample endpoint is created, with a static token. 7 | 8 | * The `server-shared` folder, instead, contains an example where the application pre-creates a REST server for its own needs, and then tells the library to re-use that server for the WHEP functionality too. A sample endpoint is created, with a callback function used to validate the token any time one is presented. 9 | 10 | Both demos subscribe to a few of the events the library can emit for debugging purposes, and serve the `web` folder as static file, which provides a basic WHEP player. Assuming the endpoint `abc123` is available at the WHEP server, you can watch it like that: 11 | 12 | http://localhost:PORT/?id=abc123 13 | 14 | where `PORT` is `7190` in the `server-owned` example, and `7090` in the `server-shared` example. Notice that, should you want Janus to send the offer (non-standard WHEP), you can do that by passing an additional `offer=false` to the query string. 15 | -------------------------------------------------------------------------------- /examples/server-owned/src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import http from 'http'; 3 | import { JanusWhepServer } from '../../../src/whep.js'; 4 | 5 | (async function main() { 6 | console.log('Example: WHEP server creating a new REST backend'); 7 | let server = null; 8 | 9 | // Create an HTTP server and bind to port 7190 just to list endpoints 10 | let myApp = express(); 11 | myApp.get('/endpoints', async (_req, res) => { 12 | res.setHeader('content-type', 'application/json'); 13 | res.status(200); 14 | res.send(JSON.stringify(server.listEndpoints())); 15 | }); 16 | myApp.get('/subscribers', async (_req, res) => { 17 | res.setHeader('content-type', 'application/json'); 18 | res.status(200); 19 | res.send(JSON.stringify(server.listSubscribers())); 20 | }); 21 | myApp.use(express.static('../web')); 22 | http.createServer({}, myApp).listen(7190); 23 | 24 | // Create a WHEP server, binding to port 7090 and using base path /whep 25 | server = new JanusWhepServer({ 26 | janus: { 27 | address: 'ws://localhost:8188' 28 | }, 29 | rest: { 30 | port: 7090, 31 | basePath: '/whep' 32 | } 33 | }); 34 | // Add a couple of global event handlers 35 | server.on('janus-disconnected', () => { 36 | console.log('WHEP server lost connection to Janus'); 37 | }); 38 | server.on('janus-reconnected', () => { 39 | console.log('WHEP server reconnected to Janus'); 40 | }); 41 | // Start the server 42 | await server.start(); 43 | 44 | // Create a test endpoint using a static token 45 | let endpoint = server.createEndpoint({ id: 'abc123', mountpoint: 1, token: 'verysecret' }); 46 | endpoint.on('new-subscriber', function() { 47 | console.log(this.id + ': Endpoint has a new subscriber'); 48 | }); 49 | endpoint.on('subscriber-gone', function() { 50 | console.log(this.id + ': Endpoint subscriber left'); 51 | }); 52 | }()); 53 | -------------------------------------------------------------------------------- /examples/server-shared/src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import http from 'http'; 3 | import { JanusWhepServer } from '../../../src/whep.js'; 4 | 5 | (async function main() { 6 | console.log('Example: WHEP server using pre-existing REST backend'); 7 | let server = null; 8 | 9 | // Create an HTTP server and bind to port 7090 10 | let myApp = express(); 11 | myApp.get('/endpoints', async (_req, res) => { 12 | res.setHeader('content-type', 'application/json'); 13 | res.status(200); 14 | res.send(JSON.stringify(server.listEndpoints())); 15 | }); 16 | myApp.get('/subscribers', async (_req, res) => { 17 | res.setHeader('content-type', 'application/json'); 18 | res.status(200); 19 | res.send(JSON.stringify(server.listSubscribers())); 20 | }); 21 | myApp.use(express.static('../web')); 22 | http.createServer({}, myApp).listen(7090); 23 | 24 | // Create a WHEP server, and add it with base path /whep to the server above 25 | server = new JanusWhepServer({ 26 | janus: { 27 | address: 'ws://localhost:8188' 28 | }, 29 | rest: { 30 | app: myApp, 31 | basePath: '/whep' 32 | } 33 | }); 34 | // Add a couple of global event handlers 35 | server.on('janus-disconnected', () => { 36 | console.log('WHEP server lost connection to Janus'); 37 | }); 38 | server.on('janus-reconnected', () => { 39 | console.log('WHEP server reconnected to Janus'); 40 | }); 41 | // Start the server 42 | await server.start(); 43 | 44 | // Create a test endpoint using a callback function to validate the token 45 | let endpoint = server.createEndpoint({ id: 'abc123', mountpoint: 1, token: function(authtoken) { 46 | return authtoken === 'verysecret'; 47 | }}); 48 | endpoint.on('new-subscriber', function() { 49 | console.log(this.id + ': Endpoint has a new subscriber'); 50 | }); 51 | endpoint.on('subscriber-gone', function() { 52 | console.log(this.id + ': Endpoint subscriber left'); 53 | }); 54 | }()); 55 | -------------------------------------------------------------------------------- /examples/web/css/demo.css: -------------------------------------------------------------------------------- 1 | .rounded { 2 | border-radius: 5px; 3 | } 4 | 5 | .centered { 6 | display: block; 7 | margin: auto; 8 | } 9 | 10 | .relative { 11 | position: relative; 12 | } 13 | 14 | .navbar-brand { 15 | margin-left: 0px !important; 16 | } 17 | 18 | .navbar-default { 19 | -webkit-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 20 | -moz-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 21 | box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 22 | } 23 | 24 | .navbar-header { 25 | padding-left: 40px; 26 | } 27 | 28 | .margin-sm { 29 | margin: 5px !important; 30 | } 31 | .margin-md { 32 | margin: 10px !important; 33 | } 34 | .margin-xl { 35 | margin: 20px !important; 36 | } 37 | .margin-bottom-sm { 38 | margin-bottom: 5px !important; 39 | } 40 | .margin-bottom-md { 41 | margin-bottom: 10px !important; 42 | } 43 | .margin-bottom-xl { 44 | margin-bottom: 20px !important; 45 | } 46 | 47 | .divider { 48 | width: 100%; 49 | text-align: center; 50 | } 51 | 52 | .divider hr { 53 | margin-left: auto; 54 | margin-right: auto; 55 | width: 45%; 56 | } 57 | 58 | .fa-2 { 59 | font-size: 2em !important; 60 | } 61 | .fa-3 { 62 | font-size: 4em !important; 63 | } 64 | .fa-4 { 65 | font-size: 7em !important; 66 | } 67 | .fa-5 { 68 | font-size: 12em !important; 69 | } 70 | .fa-6 { 71 | font-size: 20em !important; 72 | } 73 | 74 | div.no-video-container { 75 | position: relative; 76 | } 77 | 78 | .no-video-icon { 79 | width: 100%; 80 | height: 240px; 81 | text-align: center; 82 | } 83 | 84 | .no-video-text { 85 | text-align: center; 86 | position: absolute; 87 | bottom: 0px; 88 | right: 0px; 89 | left: 0px; 90 | font-size: 24px; 91 | } 92 | 93 | .meetecho-logo { 94 | padding: 12px !important; 95 | } 96 | 97 | .meetecho-logo > img { 98 | height: 26px; 99 | } 100 | 101 | pre { 102 | white-space: pre-wrap; 103 | white-space: -moz-pre-wrap; 104 | white-space: -pre-wrap; 105 | white-space: -o-pre-wrap; 106 | word-wrap: break-word; 107 | } 108 | 109 | .januscon { 110 | font-weight: bold; 111 | animation: pulsating 1s infinite; 112 | } 113 | @keyframes pulsating { 114 | 30% { 115 | color: #FFD700; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Simple WHEP server (Janus) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |

50 | WHEP Endpoint 51 |

52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple WHEP Server 2 | ================== 3 | 4 | This is a Node.js library implementation of a [WHEP server](https://datatracker.ietf.org/doc/draft-ietf-wish-whep/), developed by [Meetecho](https://www.meetecho.com), using the [Janus WebRTC Server](https://github.com/meetecho/janus-gateway/) as a WebRTC server backend and [Janode](https://github.com/meetecho/janode/) as its Janus stack. While it was initially conceived to be used mostly for testing with [Simple WHEP Client](https://github.com/meetecho/simple-whep-client) (based on [GStreamer's webrtcbin](https://gstreamer.freedesktop.org/documentation/webrtc/index.html)), as a standard WHEP implementation it's supposed to interoperate just as well with other WHEP implementations. 5 | 6 | The library is available on [npm](https://www.npmjs.com/package/janus-whep-server) and the source code is on [Github](https://github.com/meetecho/simple-whep-server/). 7 | 8 | > Note: this is an implementation of WHEP (WebRTC-HTTP egress protocol), **NOT** WHIP (WebRTC-HTTP ingestion protocol). If you're looking for a WHIP server to handle media ingestion, check the [Simple WHIP Server](https://github.com/meetecho/simple-whip-server) library instead. The two libraries can be used together in the same application, if you want to serve both protocols at the same time. 9 | 10 | # Example of usage 11 | 12 | The repo comes with a [few examples](https://github.com/meetecho/simple-whep-server/tree/master/examples) that show how you can create a new WHEP server. 13 | 14 | You create a new server this way: 15 | 16 | ```js 17 | const server = new JanusWhepServer(config); 18 | await server.start(); 19 | ``` 20 | 21 | where `config` is an object that may contain the following properties: 22 | 23 | ``` 24 | { 25 | janus: { 26 | address: '' 27 | }, 28 | rest: { 29 | app: 30 | port: , 31 | basePath: '', 32 | https: { 33 | // cert, key, passphrase; in case an HTTPS server is to be created 34 | } 35 | }, 36 | allowTrickle: , 37 | strictETags: , 38 | iceServers: [ 39 | // list of ICE servers to send back in Link headers by default, e.g. 40 | // { uri: 'stun:stun.example.net' }, 41 | // { uri: 'turn:turn.example.net?transport=udp', username: 'user', credential: 'password' }, 42 | ] 43 | } 44 | ``` 45 | 46 | The following snippet creates a WHEP server that will spawn its own REST backend on port `7090`: 47 | 48 | ```js 49 | const server = new JanusWhepServer({ 50 | janus: { 51 | address: 'ws://localhost:8188' 52 | }, 53 | rest: { 54 | port: 7090, 55 | basePath: '/whep' 56 | } 57 | }); 58 | ``` 59 | 60 | The following snippet reuses an existing Express app contest for the WHEP server: 61 | 62 | ```js 63 | const server = new JanusWhepServer({ 64 | janus: { 65 | address: 'ws://localhost:8188' 66 | }, 67 | rest: { 68 | app: myApp, 69 | basePath: '/whep' 70 | } 71 | }); 72 | ``` 73 | 74 | The `JanusWhepServer` exposes a few methods to manage endpoints that should be served by the WHEP server. This creates a new endpoint: 75 | 76 | ```js 77 | const endpoint = server.createEndpoint({ id: 'test', mountpoint: 1, token: 'verysecret' }); 78 | ``` 79 | 80 | which returns a `JanusWhepEndpoint` instance. You can also retrieve the same instance later on with a call to `getEndpoint(id)`, should you need it. 81 | 82 | The object to pass when creating a new endpoint must refer to the following structure: 83 | 84 | ``` 85 | { 86 | id: "", 87 | mountpoint: , 88 | pin: , 89 | label: