Skip to content

Kaundur/python-peerjs-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

python-peerjs-server

An asyncio-based Python port of the PeerJS signalling server, the WebSocket broker that PeerJS clients use to discover each other and exchange WebRTC offers/answers/candidates.

It's feature-complete and wire-compatible with the original: WebSocket session handling, reconnect, and the HTTP API are all implemented, plus a few deployment conveniences, like rate limiting, proxied-IP support, and bounded queues.

Features

  • WebSocket signalling at {path}/peerjs with id/token/key query-param auth
  • Reconnect support (same id + token re-attaches the socket and drains queued messages)
  • Per-peer message queueing for offline destinations, with periodic EXPIRE sweeps
  • Periodic dead-connection reaping based on heartbeat pings
  • HTTP API: GET {path}{key}/id (fresh peer id) and GET {path}{key}/peers (peer list, opt-in via allow_discovery)
  • CORS header support
  • Fixed-window per-IP rate limiting on the HTTP API
  • Optional X-Forwarded-For trust for real client IPs when running behind a reverse proxy

Requirements

  • Python 3.13+
  • websockets>=14 (the only runtime dependency, installed automatically below)

Installation

pip install python-peerjs-server

For local development (editable install with test/lint tooling):

pip install -e ".[dev]"

Usage

Run as a console script:

peerjs-server --port 9000 --key peerjs

or as a module:

python -m python_peerjs_server --port 9000 --key peerjs

CLI options

Flag Env var Default Description
--host PEERJS_HOST :: Bind host
--port PEERJS_PORT 9000 Bind port
--key PEERJS_KEY peerjs Access key clients must supply
--path PEERJS_PATH / Path prefix for HTTP/WS routes
--allow-discovery PEERJS_ALLOW_DISCOVERY off Expose GET /peers
--cors-origin PEERJS_CORS_ORIGIN unset CORS origin for HTTP API responses
--log-level PEERJS_LOG_LEVEL INFO Logging level

Other server behaviour (timeouts, rate limits, queue caps, proxy trust) is configured via python_peerjs_server.config.Config when embedding the server programmatically; see src/python_peerjs_server/config.py for the full set of fields and their defaults.

Embedding in your own asyncio app

import asyncio
from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve

asyncio.run(peerjs_serve(Config(port=9000, key="peerjs")))

Logging

If you're running via the CLI, --log-level/PEERJS_LOG_LEVEL already configures this for you (it calls logging.basicConfig); the rest of this section is for embedding python_peerjs_server in your own app.

Every module logs under the python_peerjs_server namespace (logging.getLogger(__name__)), so that one logger name is the knob to turn:

import logging

# See python_peerjs_server logs at INFO+ on stderr
logging.getLogger("python_peerjs_server").setLevel(logging.INFO)
logging.getLogger("python_peerjs_server").addHandler(logging.StreamHandler())

# Or, if your app already configured the root logger, just set the level;
# propagation does the rest
logging.getLogger("python_peerjs_server").setLevel(logging.DEBUG)

# Silence a noisy sub-logger specifically
logging.getLogger("python_peerjs_server.services.web_socket_session").setLevel(logging.WARNING)

A NullHandler is attached by default, so the library stays silent unless logging is configured (either on python_peerjs_server or higher up).

Connecting a PeerJS client

This server only implements the signalling protocol; clients connect using the PeerJS client library (docs, GitHub):

const peer = new Peer("some-id", {
    host: "localhost",
    port: 9000,
    path: "/",
    key: "peerjs",
});

Runnable versions of the standalone and FastAPI embedding patterns live in examples/: standalone.py and fastapi_app.py.

CORS

The PeerJS client calls the HTTP API (GET {path}{key}/id) before it opens the WebSocket. If your page is served from a different origin than the broker (e.g. a Vite/CRA dev server on localhost:5173 talking to a broker on localhost:9000), the browser will block that request with a CORS error unless you set --cors-origin / PEERJS_CORS_ORIGIN / Config.cors_origin to the exact origin (scheme + host + port) serving the page:

peerjs-server --port 9000 --key peerjs --cors-origin http://localhost:5173

It's unset by default (no Access-Control-Allow-Origin header sent), which is fine for same-origin deployments.

Running alongside another web framework

peerjs_serve() is just a coroutine; you can run it inside whatever process already hosts your app instead of standing up a separate service. Pass handle_signals=False so it doesn't fight your app for SIGINT/SIGTERM handling, and pick a port distinct from your main app's.

Runnable versions of both patterns below live in examples/: tornado_app.py and flask_app.py.

Tornado

Tornado runs on the standard asyncio event loop, so the broker can simply be scheduled as another task on it:

import asyncio
import tornado.ioloop
import tornado.web

from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve


class MainHandler(tornado.web.RequestHandler):
    def get(self) -> None:
        self.write("hello from tornado")


async def main() -> None:
    app = tornado.web.Application([(r"/", MainHandler)])
    app.listen(8888)

    asyncio.create_task(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))

    await asyncio.Event().wait()  # run forever


if __name__ == "__main__":
    asyncio.run(main())

Flask

Flask's dev server is sync/WSGI and blocks its own thread, so the broker has to run on an event loop in a separate thread:

import asyncio
import threading

from flask import Flask

from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve

app = Flask(__name__)


@app.get("/")
def index() -> str:
    return "hello from flask"


def run_python_peerjs_server() -> None:
    asyncio.run(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))


if __name__ == "__main__":
    threading.Thread(target=run_python_peerjs_server, daemon=True).start()
    app.run(port=8888)

For an ASGI app (FastAPI, Starlette) you can instead start it from a lifespan/startup hook with asyncio.create_task, the same way as the Tornado example: both share a loop with no extra thread needed.

Development

pip install -e ".[dev]"  # install with dev/test/lint dependencies
pytest                 # run tests
ruff check src         # lint
ruff format src        # format
mypy src                # type check
bandit -r src           # security scan

License

MIT, see LICENSE.

About

Wire-compatible asyncio port of the PeerJS signalling server, standalone or embeddable.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages