{"id":23143666,"url":"https://github.com/digitalinteraction/portals","last_synced_at":"2026-04-09T02:31:30.043Z","repository":{"id":146138396,"uuid":"549632908","full_name":"digitalinteraction/portals","owner":"digitalinteraction","description":"JavaScript library for connecting multiple peers using WebRTC","archived":false,"fork":false,"pushed_at":"2022-11-07T21:45:00.000Z","size":407,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-04T12:14:46.272Z","etag":null,"topics":["esm","husky","library","lint-staged","nodejs","npm-package","prettier","typescript","webrtc","websocket"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@openlab/portals","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/digitalinteraction.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-10-11T13:46:45.000Z","updated_at":"2024-11-08T12:32:12.000Z","dependencies_parsed_at":null,"dependency_job_id":"e7281c36-81cc-4a52-8d1a-ccf168d97429","html_url":"https://github.com/digitalinteraction/portals","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/digitalinteraction/portals","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalinteraction%2Fportals","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalinteraction%2Fportals/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalinteraction%2Fportals/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalinteraction%2Fportals/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/digitalinteraction","download_url":"https://codeload.github.com/digitalinteraction/portals/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalinteraction%2Fportals/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31582598,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"online","status_checked_at":"2026-04-09T02:00:06.848Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["esm","husky","library","lint-staged","nodejs","npm-package","prettier","typescript","webrtc","websocket"],"created_at":"2024-12-17T15:13:56.455Z","updated_at":"2026-04-09T02:31:30.025Z","avatar_url":"https://github.com/digitalinteraction.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# portals.js\n\nPortals is a pair of libraries for creating WebRTC connections between multiple parties, a \"portal\".\n\n```sh\nnpm install @openlab/portals\n```\n\n\u003e **portals.js** is ESM only.\n\n## Server\n\nAll connections in a portal are peer-to-peer, but the first step needs a signalling server to establish those connections.\nPortals provides an [agnostic library](#agnostic-library) to perform the signalling and a specific version\ntailored for Node.js and [ws](https://github.com/websockets/ws).\n\n**Node.js + express + ws**\n\n```js\nimport http from 'http'\nimport express from 'express'\nimport { NodePortalServer } from '@openlab/portals/node-server.js'\n\nconst app = express()\nconst rooms = ['coffee-chat', 'home', 'misc']\nconst server = http.createServer(app)\nconst portal = new NodePortalServer({ server, path: '/portal', rooms })\n\nserver.listen(8080, () =\u003e console.log('Listening on :8080'))\n```\n\n\u003e You'll need to `npm install express ws` to get the dependencies\n\n## Client\n\nOn the client, there is a library for connecting to the signalling server,\nconnecting to a room and handling connections and disconnections within that room.\n\n```js\nimport { NodePortalServer } from '@openlab/portals/client.js'\n\nconst rtc = {\n  iceServers: [\n    { urls: 'stun:stun1.l.google.com:19302' },\n    { urls: 'stun:stun2.l.google.com:19302' },\n  ],\n}\n\nasync function main() {\n  // Determine the room to join somehow\n  const url = new URL(location.href)\n  const room = url.searchParams.get('room')\n\n  // Create a WebSocket URL to the PortalServer\n  const server = new URL('portal', location.href)\n  server.protocol = server.protocol.replace(/^http/, 'ws')\n\n  // Request a MediaStream from the client's webcam\n  const stream = await navigator.mediaDevices.getUserMedia({\n    video: { width: 1280, height: 720 },\n  })\n\n  // Create the portal gun\n  const portalGun = new PortalGun({ room, url: server, rtc })\n\n  portalGun.addEventListener('connection', (portal) =\u003e {\n    // Add the local MediaStream to send video through the portal\n    // Note: This API is currently unstable\n    portal.addMediaStream(stream)\n\n    // Listen for tracks from the peer to recieve video through the portal\n    portal.peer.addEventListener('track', (event) =\u003e {\n      event.track.onunmute = () =\u003e {\n        console.debug('@connected', portal.target.id, event.streams[0])\n        // Render the track somehow\n      }\n    })\n\n    portal.addEventListener('error', (error) =\u003e {\n      // Handle an error connecting to this peer\n      // e.g. present the error to the user\n    })\n  })\n\n  // Stop rendering a peer that has disconnected\n  portalGun.addEventListener('disconnection', (portal) =\u003e {\n    console.debug('@disconnection', portal.target.id, null)\n  })\n}\n\nmain()\n```\n\nYou create a `PortalGun` which is responsible for talking to the signalling server and telling you about new and closed connections, known as \"portals\".\n\nWhen you recieve a new portal, through `\"connection\"`, you pass it the local `MediaStream` and listen for tracks that it is sending. Its up to you what to do with those streams.\nYou have `portal.target.id` which is a unique id for that portal.\n\nWhen a portal closes, through `\"disconnection\"`, you can clean up any rendering you previously did for it.\n\nThe `rtc` config is directly passed to the constructor of [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection), so you can put any of those parameters in there.\n\n## Full example\n\nFor a detailed example, see [example](./example) which is a full use of `NodePortalServer` on the backend and `PortalGun` on the frontend. The example is a local-cctv like system where any user that joins the room is added to a video-only screen where everyone can see everyone else.\n\nIt has a small server-side hack to bundle the client-side code using [esbuild](https://esbuild.github.io/).\n\n## Agnostic library\n\nIf you don't want to use Node.js or want to use different libraries,\nthere is an an agnostic version of the server.\nBelow is an example for hooking up the portals library with a hypothetical socket server.\nIt needs to call these methods at the right times:\n\n- `onConnection` - when a new socket has connected, it should implement `Traveller`\n- `onMessage` - when a socket recieved a message from the portals client\n- `onClose` - when a socket disconnected\n\nThere are extra hooks for debugging, exposed as events:\n\n- `error` (Error) - emits when an error occured\n- `debug` (string) - outputs debug messages\n\n```ts\nimport { CustomSocketServer, CustomSocket } from 'custom-socket-lib'\nimport { PortalServer, Traveller } from '@openlab/portals/server.js'\n\nconst portals = new PortalServer({ rooms: ['home'] })\nfunction getConnection(socket: CustomSocket, request: Request): Traveller {\n  const { id, room } = '/* get from socket */'\n  function send(type, payload, from) {\n    socket.send(JSON.stringify({ type, [type]: payload, from }))\n  }\n  return { id, room, send }\n}\n\nconst sockets = new CustomSocketServer(/* ... */)\nsockets.addEventListener('connection', (socket) =\u003e {\n  // 1. Tell portals about the new connection\n  const connection = getConnection(socket)\n  portals.onConnection(connection)\n\n  // 2. Tell portals about new messages\n  socket.addEventListener('message', (message) =\u003e {\n    const { type, [type]: payload, target = null } = JSON.parse(message)\n    portals.onMessage(connection, type, payload, target)\n  })\n\n  // 3. Tell portals about connections closing\n  socket.addEventListener('close', () =\u003e {\n    portals.onClose(connection)\n  })\n})\n\n// Optionally debug things\nportals.addEventListener('error', (error) =\u003e console.error(error))\nportals.addEventListener('debug', (message) =\u003e console.debug(message))\n```\n\nYou wrap your library's transport into a `Traveller` object that the library\nwill store and use to send messages to clients.\nYou can get the `id` or `room` from the connecting client or you can generate them.\nThe client expects WebSocket messages in a certain format, defined below,\nso the `send` method should format them in this way.\n\n\u003e `crypto.randomUUID()` is good for generating client ids.\n\nWhen a client disconnects you need to tell the PortalServer with `onClose` and pass the traveller object.\n\nWhen a client recieves a message, tell the PortalServer with `onMessage`.\nThe client has a default payload structure, defined below.\n\n**client → server messages**\n\nThe client sends messages up to the server as JSON.\nIt always has `type` as a string, then the \"type\" is used as a key is used to put the payload in.\n`target` is optional and may contain the id of the specific client they want to talk to.\n\n```json\n{ \"type\": \"ice\", \"ice\": { \"key\": \"value\" }, \"target\": \"abcdef\" }\n```\n\nOn the client, you can pass a `composeMessage` method to provide your own format.\n\n**server → client messages**\n\nThe server sends messages down to the client as JSON.\nIt always has `type` as a string, then the \"type\" is used as a key is used to put the payload in.\n`from` is optional and may contain the id of the specific client that send the message.\n\n```json\n{ \"type\": \"ice\", \"ice\": { \"key\": \"value\" }, \"from\": \"abcdef\" }\n```\n\nOn the client, you can pass a `parseMessage` method to parse a custom format your server might use.\n\n## Signals\n\nThere are types for the different messages that are sent up and down from the server, these are currently in [src/lib.ts](./src/lib.ts)\n\n**InfoSignal**\n\nSent to clients when members join and leave the room.\nIt contains the id of the client, the other members in the room\nand whether to be \"nice\" to them or not\n(used as part of the WebRTC call).\n\n**ErrorSignal**\n\nWhen the client did something wrong, it has an error\nwhich the client could use to rectify it, or let the developer know what to fix. Error codes:\n\n- `room_not_set` - sent when a client joined and didn't set the `?room` URL parameter.\n- `room_not_found` - sent when a client joined, messaged or left with a room that doesn't exist.\n\n**DescriptionSignal**\n\nUsed as part of the WebRTC calling, [more info](https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription)\n\n**CandidateSignal**\n\nUsed as part of the WebRTC calling, [more info](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate)\n\n## Future work / ideas\n\n- designing the server to be horizontally scalable\n  - can rooms be in redis somehow, or backed by socket.io?\n- dynamic rooms / API for existing rooms\n- Data connections\n- add automated tests\n- explore Deno usage\n- pull rtc configuration from the server?\n- simplify payload structure?\n- explore what happens when MediaStreams break\n- abstract away from WebSockets?\n- what to do with signaller 'disconnection' / 'reconnection' events\n  - rename current portal events to 'opened' / 'closed' ?\n- better name for 'signaller'\n- enable custom signals for library-users to use\n\n## Non-goals\n\n- Anything not peer-to-peer or more complicated topologies\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdigitalinteraction%2Fportals","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdigitalinteraction%2Fportals","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdigitalinteraction%2Fportals/lists"}