├── .deepsource.toml
├── .github
├── FUNDING.yml
└── workflows
│ ├── browserstack.yml
│ ├── codeql-analysis.yml
│ ├── prettier.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.toml
├── .releaserc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __test__
├── faker.ts
├── logger.spec.ts
├── peer.spec.ts
├── setup.ts
└── util.spec.ts
├── e2e
├── .eslintrc
├── alice.html
├── bob.html
├── commit_data.js
├── data.js
├── datachannel
│ ├── Int32Array.js
│ ├── Int32Array_as_ArrayBuffer.js
│ ├── Int32Array_as_Uint8Array.js
│ ├── TypedArrayView_as_ArrayBuffer.js
│ ├── Uint8Array.js
│ ├── Uint8Array_as_ArrayBuffer.js
│ ├── arraybuffers.js
│ ├── arraybuffers_as_uint8array.js
│ ├── arrays.js
│ ├── arrays_unchunked.js
│ ├── blobs.js
│ ├── dates.js
│ ├── dates_as_json_string.js
│ ├── dates_as_string.js
│ ├── files.js
│ ├── long_string.js
│ ├── numbers.js
│ ├── objects.js
│ ├── serialization.html
│ ├── serialization.js
│ ├── serialization.page.ts
│ ├── serializationTest.ts
│ ├── serialization_binary.spec.ts
│ ├── serialization_json.spec.ts
│ ├── serialization_msgpack.spec.ts
│ ├── strings.js
│ └── typed_array_view.js
├── mediachannel
│ ├── close.html
│ ├── close.js
│ ├── close.page.ts
│ └── close.spec.ts
├── package.json
├── peer
│ ├── disconnected.html
│ ├── id-taken.html
│ ├── peer.page.ts
│ ├── peer.spec.ts
│ └── server-unavailable.html
├── style.css
├── tsconfig.json
├── types.d.ts
├── wdio.bstack.conf.ts
├── wdio.local.conf.ts
└── wdio.shared.conf.ts
├── jest.config.cjs
├── lib
├── api.ts
├── baseconnection.ts
├── dataconnection
│ ├── BufferedConnection
│ │ ├── BinaryPack.ts
│ │ ├── BufferedConnection.ts
│ │ ├── Json.ts
│ │ ├── Raw.ts
│ │ └── binaryPackChunker.ts
│ ├── DataConnection.ts
│ └── StreamConnection
│ │ ├── MsgPack.ts
│ │ └── StreamConnection.ts
├── encodingQueue.ts
├── enums.ts
├── exports.ts
├── global.ts
├── logger.ts
├── mediaconnection.ts
├── msgPackPeer.ts
├── negotiator.ts
├── optionInterfaces.ts
├── peer.ts
├── peerError.ts
├── servermessage.ts
├── socket.ts
├── supports.ts
├── util.ts
└── utils
│ ├── randomToken.ts
│ └── validateId.ts
├── package-lock.json
├── package.json
├── renovate.json
└── tsconfig.json
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | test_patterns = ["test/**"]
4 |
5 | [[analyzers]]
6 | name = "javascript"
7 | enabled = true
8 |
9 | [analyzers.meta]
10 | environment = [
11 | "nodejs",
12 | "mocha"
13 | ]
14 | dialect = "typescript"
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: peers
2 | open_collective: peer
3 |
--------------------------------------------------------------------------------
/.github/workflows/browserstack.yml:
--------------------------------------------------------------------------------
1 | name: "BrowserStack Test"
2 | on: [push, pull_request]
3 |
4 | concurrency:
5 | group: browserstack
6 | jobs:
7 | ubuntu-job:
8 | name: "BrowserStack Test on Ubuntu"
9 | runs-on: ubuntu-latest # Can be self-hosted runner also
10 | steps:
11 | - name: "BrowserStack Env Setup" # Invokes the setup-env action
12 | uses: browserstack/github-actions/setup-env@master
13 | with:
14 | username: ${{ secrets.BROWSERSTACK_USERNAME }}
15 | access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
16 |
17 | - name: "BrowserStack Local Tunnel Setup" # Invokes the setup-local action
18 | uses: browserstack/github-actions/setup-local@master
19 | with:
20 | local-testing: "start"
21 | local-logging-level: "all-logs"
22 | local-identifier: "random"
23 |
24 | # The next 3 steps are for building the web application to be tested and starting the web server on the runner environment
25 |
26 | - name: "Checkout the repository"
27 | uses: actions/checkout@v4
28 |
29 | - name: "Building web application to be tested"
30 | run: npm install && npm run build
31 |
32 | - name: "Running application under test"
33 | run: npx http-server -p 3000 --cors &
34 |
35 | - name: "Running test on BrowserStack" # Invokes the actual test script that would run on BrowserStack browsers
36 | run: npm run e2e:bstack # See sample test script above
37 | env:
38 | BYPASS_WAF: ${{ secrets.BYPASS_WAF }}
39 |
40 | - name: "BrowserStackLocal Stop" # Terminating the BrowserStackLocal tunnel connection
41 | uses: browserstack/github-actions/setup-local@master
42 | with:
43 | local-testing: "stop"
44 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [master, rc, stable]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [master]
20 | schedule:
21 | - cron: "15 2 * * 5"
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: ["javascript"]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v4
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v3
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 |
52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 | # queries: security-extended,security-and-quality
54 |
55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
56 | # If this step fails, then you should remove it and run the build manually (see below)
57 | - name: Autobuild
58 | uses: github/codeql-action/autobuild@v3
59 |
60 | # ℹ️ Command-line programs to run using the OS shell.
61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
62 |
63 | # If the Autobuild fails above, remove it and uncomment the following three lines.
64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
65 |
66 | # - run: |
67 | # echo "Run, Build Application using script"
68 | # ./location_of_script_within_repo/buildscript.sh
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v3
72 |
--------------------------------------------------------------------------------
/.github/workflows/prettier.yml:
--------------------------------------------------------------------------------
1 | # From https://til.simonwillison.net/github-actions/prettier-github-actions
2 | name: Check JavaScript for conformance with Prettier
3 |
4 | on:
5 | push:
6 | pull_request:
7 |
8 | jobs:
9 | prettier:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check out repo
13 | uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 16
17 | cache: "npm"
18 | - run: npm ci
19 | - name: Run prettier
20 | run: |-
21 | npm run format:check
22 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - rc
6 | - stable
7 | jobs:
8 | release:
9 | name: Release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: "lts/*"
20 | - name: Install dependencies
21 | run: npm ci
22 | - name: Import GPG key
23 | id: import_gpg
24 | uses: crazy-max/ghaction-import-gpg@v6
25 | with:
26 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
27 | passphrase: ${{ secrets.GPG_PASSPHRASE }}
28 | git_user_signingkey: true
29 | git_commit_gpgsign: true
30 | - name: Release
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
34 | GIT_COMMITTER_NAME: ${{ steps.import_gpg.outputs.name }}
35 | GIT_COMMITTER_EMAIL: ${{ steps.import_gpg.outputs.email }}
36 | run: npx semantic-release
37 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: ["master"]
9 | pull_request:
10 | branches: ["master"]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [16.x, 18.x, 20.x]
19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: "npm"
28 | - run: npm ci
29 | - run: npm run check
30 | - run: npm run build
31 | # - run: npm run lint
32 | - run: npm run coverage
33 | - name: Publish code coverage to CodeClimate
34 | uses: paambaati/codeclimate-action@v6.0.0
35 | env:
36 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | coverage
3 | *.seed
4 | *.log
5 | *.csv
6 | *.dat
7 | *.out
8 | *.pid
9 | *.[t]gz
10 |
11 | dist
12 | pids
13 | logs
14 | results
15 | demo
16 | bower.json
17 | node_modules
18 | .parcel-cache
19 | .idea
20 | npm-debug.log
21 | .DS_STORE
22 |
23 | test/output
24 | browserstack.err
25 | .tscache
26 | test/public
27 | .vscode/
28 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | docs
3 | package-json.lock
4 |
5 | # semantic-release
6 | CHANGELOG.md
--------------------------------------------------------------------------------
/.prettierrc.toml:
--------------------------------------------------------------------------------
1 | trailingComma = "all"
2 | semi = true
3 | useTabs = true
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["stable", { "name": "rc", "prerelease": true }],
3 | "plugins": [
4 | "@semantic-release/commit-analyzer",
5 | "@semantic-release/release-notes-generator",
6 | "@semantic-release/changelog",
7 | "@semantic-release/npm",
8 | "@semantic-release/git",
9 | "@semantic-release/github"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.5.4](https://github.com/peers/peerjs/compare/v1.5.3...v1.5.4) (2024-05-14)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * **deps:** update dependency webrtc-adapter to v9 ([#1266](https://github.com/peers/peerjs/issues/1266)) ([5536abf](https://github.com/peers/peerjs/commit/5536abf8d6345c248df875e0e22c520a20cb2919))
7 | * remove CBOR ([badc9e8](https://github.com/peers/peerjs/commit/badc9e8bc4f7ce5517de3a58abcaec1d566eccf5)), closes [#1271](https://github.com/peers/peerjs/issues/1271) [#1247](https://github.com/peers/peerjs/issues/1247) [#1271](https://github.com/peers/peerjs/issues/1271)
8 |
9 | ## [1.5.3](https://github.com/peers/peerjs/compare/v1.5.2...v1.5.3) (2024-05-11)
10 |
11 |
12 | ### Bug Fixes
13 |
14 | * navigator is not defined. ([#1202](https://github.com/peers/peerjs/issues/1202)) ([4b7a74d](https://github.com/peers/peerjs/commit/4b7a74d74c50461fde80e84992d88a9d564dbe72)), closes [#1165](https://github.com/peers/peerjs/issues/1165)
15 | * remove need for `unsafe-eval` ([3fb31b3](https://github.com/peers/peerjs/commit/3fb31b316b8f4d699d087e1b465e908688be3872))
16 |
17 | ## [1.5.2](https://github.com/peers/peerjs/compare/v1.5.1...v1.5.2) (2023-12-05)
18 |
19 |
20 | ### Bug Fixes
21 |
22 | * support Blobs nested in objects ([7956dd6](https://github.com/peers/peerjs/commit/7956dd640388fce62c83453d56e1a20aec2212b2)), closes [#1163](https://github.com/peers/peerjs/issues/1163)
23 |
24 | ## [1.5.1](https://github.com/peers/peerjs/compare/v1.5.0...v1.5.1) (2023-09-23)
25 |
26 |
27 | ### Bug Fixes
28 |
29 | * convert `Blob`s to `ArrayBuffer`s during `.send()` ([95bb0f7](https://github.com/peers/peerjs/commit/95bb0f7fa9aa0d119613727c32857e5af33e14a1)), closes [#1137](https://github.com/peers/peerjs/issues/1137)
30 | * convert `Blob`s to `ArrayBuffer`s during `.send()` ([#1142](https://github.com/peers/peerjs/issues/1142)) ([094f849](https://github.com/peers/peerjs/commit/094f849816d327bf74a447fbf7d58195c1a4fc66))
31 |
32 | # [1.5.0](https://github.com/peers/peerjs/compare/v1.4.7...v1.5.0) (2023-09-03)
33 |
34 |
35 | ### Bug Fixes
36 |
37 | * **datachannel:** sending order is now preserved correctly ([#1038](https://github.com/peers/peerjs/issues/1038)) ([0fb6179](https://github.com/peers/peerjs/commit/0fb61792ed3afe91123550a159c8633ed0976f89)), closes [#746](https://github.com/peers/peerjs/issues/746)
38 | * **deps:** update dependency @swc/helpers to ^0.4.0 ([a7de8b7](https://github.com/peers/peerjs/commit/a7de8b78f57a5cf9708fa54e9f82f4ab43c0bca2))
39 | * **deps:** update dependency cbor-x to v1.5.4 ([c1f04ec](https://github.com/peers/peerjs/commit/c1f04ecf686e64266fb54b3e4992c73c1522ae79))
40 | * **deps:** update dependency eventemitter3 to v5 ([caf01c6](https://github.com/peers/peerjs/commit/caf01c6440534cbe190facd84cecf9ca62e4a5ce))
41 | * **deps:** update dependency peerjs-js-binarypack to v1.0.2 ([7452e75](https://github.com/peers/peerjs/commit/7452e7591d4982d9472c524d6ad30e66c2a2b44f))
42 | * **deps:** update dependency webrtc-adapter to v8 ([431f00b](https://github.com/peers/peerjs/commit/431f00bd89809867a19c98224509982b82769558))
43 | * **deps:** update dependency webrtc-adapter to v8.2.2 ([62402fc](https://github.com/peers/peerjs/commit/62402fcae03c78382d7fa80c11f459aca8d21620))
44 | * **deps:** update dependency webrtc-adapter to v8.2.3 ([963455e](https://github.com/peers/peerjs/commit/963455ee383a069e53bd93b1128d82615a698245))
45 | * **MediaConnection:** `close` event is fired on remote Peer ([0836356](https://github.com/peers/peerjs/commit/0836356d18c91449f4cbb23e4d4660a4351d7f56)), closes [#636](https://github.com/peers/peerjs/issues/636) [#1089](https://github.com/peers/peerjs/issues/1089) [#1032](https://github.com/peers/peerjs/issues/1032) [#832](https://github.com/peers/peerjs/issues/832) [#780](https://github.com/peers/peerjs/issues/780) [#653](https://github.com/peers/peerjs/issues/653)
46 | * **npm audit:** Updates all dependencies that cause npm audit to issue a warning ([6ef5707](https://github.com/peers/peerjs/commit/6ef5707dc85d8b921d8dfea74890b110ddf5cd4f))
47 |
48 |
49 | ### Features
50 |
51 | * `.type` property on `Error`s emitted from connections ([#1126](https://github.com/peers/peerjs/issues/1126)) ([debe7a6](https://github.com/peers/peerjs/commit/debe7a63474b9cdb705676d4c7892b0cd294402a))
52 | * `PeerError` from connections ([ad3a0cb](https://github.com/peers/peerjs/commit/ad3a0cbe8c5346509099116441e6c3ff0b6ca6c4))
53 | * **DataConnection:** handle close messages and flush option ([6ca38d3](https://github.com/peers/peerjs/commit/6ca38d32b0929745b92a55c8f6aada1ee0895ce7)), closes [#982](https://github.com/peers/peerjs/issues/982)
54 | * **MediaChannel:** Add experimental `willCloseOnRemote` event to MediaConnection. ([ed84829](https://github.com/peers/peerjs/commit/ed84829a1092422f3d7f92f467bcf5b8ada82891))
55 | * MsgPack/Cbor serialization ([fcffbf2](https://github.com/peers/peerjs/commit/fcffbf243cb7d6dabfc773211c155c0ae1e00baf))
56 | * MsgPack/Cbor serialization ([#1120](https://github.com/peers/peerjs/issues/1120)) ([4367256](https://github.com/peers/peerjs/commit/43672564ee9edcb15e736b0333c6ad8aeae20c59)), closes [#611](https://github.com/peers/peerjs/issues/611)
57 |
58 | ## [1.4.7](https://github.com/peers/peerjs/compare/v1.4.6...v1.4.7) (2022-08-09)
59 |
60 |
61 | ### Bug Fixes
62 |
63 | * **browser-bundle:** Leaked private functions in the global scope ([857d425](https://github.com/peers/peerjs/commit/857d42524a929388b352a2330f18fdfc15df6c22)), closes [#989](https://github.com/peers/peerjs/issues/989)
64 |
65 | ## [1.4.6](https://github.com/peers/peerjs/compare/v1.4.5...v1.4.6) (2022-05-25)
66 |
67 |
68 | ### Bug Fixes
69 |
70 | * **typings:** `MediaConnection.answer()` doesn’t need a `stream` anymore, thanks [@matallui](https://github.com/matallui)! ([666dcd9](https://github.com/peers/peerjs/commit/666dcd9770fe080e00898b9138664e8996bf5162))
71 | * **typings:** much stronger event typings for `DataConnection`,`MediaConnection` ([0c96603](https://github.com/peers/peerjs/commit/0c96603a3f97f28eabe24906e692c31ef0ebca13))
72 |
73 |
74 | ### Performance Improvements
75 |
76 | * **turn:** reduce turn server count ([8816f54](https://github.com/peers/peerjs/commit/8816f54c4b4bff5f6bd0c7ccf5327ec84e80a8ca))
77 |
78 | ## [1.4.5](https://github.com/peers/peerjs/compare/v1.4.4...v1.4.5) (2022-05-24)
79 |
80 |
81 | ### Bug Fixes
82 |
83 | * **referrerPolicy:** you can now set a custom referrerPolicy for api requests ([c0ba9e4](https://github.com/peers/peerjs/commit/c0ba9e4b64f233c2733a8c5e904a8536ae37eb42)), closes [#955](https://github.com/peers/peerjs/issues/955)
84 | * **typings:** add missing type exports ([#959](https://github.com/peers/peerjs/issues/959)) ([3c915d5](https://github.com/peers/peerjs/commit/3c915d57bb18ac822d3438d879717266ee84b635)), closes [#961](https://github.com/peers/peerjs/issues/961)
85 |
86 | ## [1.4.4](https://github.com/peers/peerjs/compare/v1.4.3...v1.4.4) (2022-05-13)
87 |
88 |
89 | ### Bug Fixes
90 |
91 | * **CRA@4:** import hack ([41c3ba7](https://github.com/peers/peerjs/commit/41c3ba7b2ca6adc226efd0e2add546a570a4aa3a)), closes [#954](https://github.com/peers/peerjs/issues/954)
92 | * **source maps:** enable source map inlining ([97a724b](https://github.com/peers/peerjs/commit/97a724b6a1e04817d79ecaf91d4384ae3a94cf99))
93 |
94 | ## [1.4.3](https://github.com/peers/peerjs/compare/v1.4.2...v1.4.3) (2022-05-13)
95 |
96 |
97 | ### Bug Fixes
98 |
99 | * **typings:** export interfaces ([979e695](https://github.com/peers/peerjs/commit/979e69545cc2fe10c60535ac9793140ef8dba4ec)), closes [#953](https://github.com/peers/peerjs/issues/953)
100 |
101 | ## [1.4.2](https://github.com/peers/peerjs/compare/v1.4.1...v1.4.2) (2022-05-12)
102 |
103 |
104 | ### Bug Fixes
105 |
106 | * **bundler import:** enable module target ([b5beec4](https://github.com/peers/peerjs/commit/b5beec4a07827f82c5e50c79c71a8cfb1ec3c40e)), closes [#761](https://github.com/peers/peerjs/issues/761)
107 |
108 | ## [1.4.1](https://github.com/peers/peerjs/compare/v1.4.0...v1.4.1) (2022-05-11)
109 |
110 |
111 | ### Bug Fixes
112 |
113 | * **old bundlers:** include support for Node 10 (EOL since 2021-04-01) / old bundlers ([c0f4648](https://github.com/peers/peerjs/commit/c0f4648b1c104e5e0e5967bb239c217288aa83e0)), closes [#952](https://github.com/peers/peerjs/issues/952)
114 |
115 | # [1.4.0](https://github.com/peers/peerjs/compare/v1.3.2...v1.4.0) (2022-05-10)
116 |
117 |
118 | ### Bug Fixes
119 |
120 | * add changelog and npm version to the repo ([d5bd955](https://github.com/peers/peerjs/commit/d5bd9552daf5d42f9d04b3087ddc34c729004daa))
121 | * add token to PeerJSOption type definition ([e7675e1](https://github.com/peers/peerjs/commit/e7675e1474b079b2804167c70335a6c6e2b8ec08))
122 | * websocket connection string ([82b8c71](https://github.com/peers/peerjs/commit/82b8c713bc03be34c2526bdf442a583c4d547c83))
123 |
124 |
125 | ### Features
126 |
127 | * upgrade to Parcel@2 ([aae9d1f](https://github.com/peers/peerjs/commit/aae9d1fa37731d0819f93535b8ad78fe4b685d1e)), closes [#845](https://github.com/peers/peerjs/issues/845) [#859](https://github.com/peers/peerjs/issues/859) [#552](https://github.com/peers/peerjs/issues/552) [#585](https://github.com/peers/peerjs/issues/585)
128 |
129 |
130 | ### Performance Improvements
131 |
132 | * **turn:** lower TURN-latency due to more local servers ([a412ea4](https://github.com/peers/peerjs/commit/a412ea4984a46d50de8873904b7067897b0f29f9))
133 |
134 |
135 |
136 | ## 1.3.2 (2021-03-11)
137 |
138 | - fixed issues #800, #803 in PR #806, thanks @jordanaustin
139 | - updated devDeps: `typescript` to 4.2
140 |
141 |
142 |
143 | ## 1.3.1 (2020-07-11)
144 |
145 | - fixed: map file resolving
146 | - removed: @types/webrtc because it contains in ts dom lib.
147 |
148 |
149 |
150 | ## 1.3.0 (2020-07-03)
151 |
152 | - changed: don't close the Connection if `iceConnectionState` changed to `disconnected`
153 |
154 |
155 |
156 | ## 1.2.0 (2019-12-24)
157 |
158 | - added: ability to change json stringify / json parse methods for DataConnection #592
159 |
160 | - removed: `peerBrowser` field from `dataConnection` because unused
161 |
162 | - fixed: lastServerId and reconnect #580 #534 #265
163 |
164 |
165 |
166 | ## 1.1.0 (2019-09-16)
167 |
168 | - removed: deprecated `RtpDataChannels` and `DtlsSrtpKeyAgreement` options
169 | - removed: grunt from deps, upgrade deps versions
170 | - removed: Reliable dep because modern browsers supports `RTCDataChannel.ordered` property
171 |
172 | - added: TURN server to default config
173 |
174 | - fixed: emit error message, then destroy/disconnect when error occurred
175 | - fixed: use `peerjs-js-binarypack` instead of `js-binarypack`
176 | - fixed: sending large files via DataConnection #121
177 |
178 |
179 |
180 | ## 1.0.4 (2019-08-31)
181 |
182 | - fixed: 'close' event for DataConnection #568
183 |
184 |
185 |
186 | ## 1.0.3 (2019-08-21)
187 |
188 | - add pingInterval option
189 |
190 |
191 |
192 | ## 1.0.2 (2019-07-20)
193 |
194 | ### Bug Fixes
195 |
196 | - fixed: memory leak in DataConnection #556
197 | - fixed: missing sdpMid in IceServer #550
198 |
199 | ### Other
200 |
201 | - updated: old @types/webrtc dependency #549
202 |
203 |
204 |
205 | ## 1.0.1 (2019-07-09)
206 |
207 | ### Bug Fixes
208 |
209 | - fixed: readyState of undefined #520
210 | - fixed: call sdpTransform in Answer #524
211 | - fixed: sdpTransform does not apply to makeAnswer SDP #523
212 |
213 |
214 |
215 | ## 1.0.0 (2019-04-10)
216 |
217 | ### Refactoring
218 |
219 | Almost all project was refactored!!!
220 |
221 | - removed: xhr long-pooling #506
222 | - changed: fetch api instead of xhr
223 |
224 | ### Features
225 |
226 | - added: heartbeat #502
227 |
228 | ### Bug Fixes
229 |
230 | - fixed: destroy RTCPeerConnection #513
231 | - fixed: MediaStream memory leak #514
232 |
233 |
234 |
235 | ## 0.3.18 (2018-10-30)
236 |
237 | ### Features
238 |
239 | - **typescript:** First commit ([0c77a5b](https://github.com/peers/peerjs/commit/0c77a5b))
240 |
241 |
242 |
243 | ## 0.3.16 (2018-08-21)
244 |
245 | ### Bug Fixes
246 |
247 | - fixed typo in README ([f1bd47e](https://github.com/peers/peerjs/commit/f1bd47e))
248 |
249 | ## Version 0.3.14
250 |
251 | - Patch for #246, which started as of Chrome 38.
252 |
253 | ## Version 0.3.11 (28 Sep 2014)
254 |
255 | - Browserify build system
256 |
257 | ## Version 0.3.10 (29 Aug 2014)
258 |
259 | - Fixed a bug where `disconnected` would be emitted for XHR requests that were aborted on purpose.
260 |
261 | ## Version 0.3.9 (11 July 2014)
262 |
263 | - Allow an external adapter to be used (for `RTCPeerConnection` and such). (Thanks, @khankuan!)
264 | - Fixed a bug where `_chunkedData` was not being cleared recursively, causing memory to be eaten up unnecessarily. (Thanks, @UnsungHero97!)
265 | - Added `peer.reconnect()`, which allows a peer to reconnect to the signalling server with the same ID it had before after it has been disconnected. (Thanks, @jure, for the amazing input :)!)
266 | - Added previously-missing error types, such as `webrtc`, `network`, and `peer-unavailable` error types. (Thanks, @mmis1000 for reporting!)
267 | - Fixed a bug where the peer would infinitely attempt to start XHR streaming when there is no network connection available. Now, the peer will simply emit a `network` error and disconnect. (Thanks, @UnsungHero97 for reporting!)
268 |
269 | ## Version 0.3.8 beta (18 Mar 2014)
270 |
271 | - **The following changes are only compatible with PeerServer 0.2.4.**
272 | - Added the ability to specify a custom path when connecting to a self-hosted
273 | PeerServer.
274 | - Added the ability to retrieve a list of all peers connected to the server.
275 |
276 | ## Version 0.3.7 beta (23 Dec 2013)
277 |
278 | - Chrome 31+/Firefox 27+ DataConnection interop for files.
279 | - Deprecate `binary-utf8` in favor of faster support for UTF8 in the regular
280 | `binary` serialization.
281 | - Fix `invalid-key` error message.
282 |
283 | ## Version 0.3.6 beta (3 Dec 2013)
284 |
285 | - Workaround for hitting Chrome 31+ buffer limit.
286 | - Add `.bufferSize` to DataConnection to indicate the size of the buffer queue.
287 | - Add `.dataChannel` to DataConnection as an alias for `._dc`, which contains
288 | the RTCDataChannel object associated with the DataConnection.
289 | - Update BinaryPack dependency.
290 |
291 | ## Version 0.3.5 beta (26 Nov 2013)
292 |
293 | - Fix bug where chunks were being emitted.
294 |
295 | ## Version 0.3.4 beta (11 Nov 2013)
296 |
297 | - Fix file transfer issue in Chrome by chunking for data over 120KB.
298 | - Use binary data when possible.
299 | - Update BinaryPack dependency to fix inefficiencies.
300 |
301 | ## Version 0.3.3 beta (2 Nov 2013)
302 |
303 | - Fix exceptions when peer emits errors upon creation
304 | - Remove extra commas
305 |
306 | ## Version 0.3.2 beta (25 Oct 2013)
307 |
308 | - Use SCTP in Chrome 31+.
309 | - Work around Chrome 31+ tab crash. The crashes were due to Chrome's lack of support for the `maxRetransmits` parameter for modifying SDP.
310 | - Fix exceptions in Chrome 29 and below.
311 | - DataChannels are unreliable by default in Chrome 30 and below. In setting
312 | reliable to `true`, the reliable shim is used only in Chrome 30 and below.
313 |
314 | ## Version 0.3.1 beta (19 Oct 2013)
315 |
316 | - Updated docs and examples for TURN server usage
317 | - Fixed global variable leak
318 | - DataConnections now have reliable: false by default. This will switch to on when reliable: true works in more browsers
319 |
320 | ## Version 0.3.0 beta (20 Sept 2013)
321 |
322 | ### Highlights
323 |
324 | - Support for WebRTC video and audio streams in both Firefox and Chrome.
325 | - Add `util.supports.[FEATURE]` flags, which represent the WebRTC features
326 | supported by your browser.
327 | - **Breaking:** Deprecate current `Peer#connections` format. Connections will no longer be
328 | keyed by label and will instead be in a list.
329 |
330 | ### Other changes
331 |
332 | - **Breaking:** Deprecate `Peer.browser` in favor of `util.browser`.
333 | - Additional logging levels (warnings, errors, all).
334 | - Additional logging functionality (`logFunction`).
335 | - SSL option now in config rather than automatic.
336 |
337 | ## Version 0.2.8 (1 July 2013)
338 |
339 | - Fix bug, no error on Firefox 24 due to missing error callback.
340 | - TLS secure PeerServers now supported.
341 | - Updated version of Reliable shim.
342 |
343 | ## Version 0.2.7 (28 May 2013)
344 |
345 | - Fix bug, no error when .disconnect called in before socket connection established.
346 | - Fix bug, failure to enter debug mode when aborting because browser not supported.
347 |
348 | ## Version 0.2.6 (2 May 2013)
349 |
350 | - Peer.browser to check browser type.
351 | - Update Reliable library and fix Reliable functionality in Chrome.
352 |
353 | ## Version 0.2.5 (24 Apr 2013)
354 |
355 | - **Firefox compatibility for Firefox Nightly.**
356 | - Misc bug fixes.
357 |
358 | ## Version 0.2.1 (3 Apr 2013)
359 |
360 | - **Warning**: this build changes the error of type `peer-destroyed` to `server-disconnected`.
361 | - ~~**Firefox compatibility.**~~ - Pushed back due to volatility of Firefox Nightly DataChannel APIs.
362 | - Browser detection added. If an incompatible browser is detected, the `browser-incompatible` error is emitted from the `Peer`.
363 | - Added a `.disconnect()` method to `Peer`, which can be called to close connections to the PeerServer (but not any active DataConnections).
364 |
365 | ## Version 0.2.0 (24 Mar 2013)
366 |
367 | - **Warning**: this build introduces the following API changes that may break existing code.
368 | - `peer.connections` is no longer a hash mapping peer IDs to connections.
369 | - Connections no longer emit errors from `PeerConnection`; `PeerConnection` errors are now forwarded to the `Peer` object.
370 | - Add support for multiple DataConnections with different labels.
371 | - Update Reliable version to support faster file transfer.
372 | - Fix bug where using XHR streaming to broker a connection occasionally fails.
373 |
374 | ## Version 0.1.7 (6 Mar 2013)
375 |
376 | - Add experimental `reliable` messaging option. [See documentation.](https://github.com/peers/peerjs/blob/master/docs/api.md#experimental-reliable-and-large-file-transfer)
377 | - Fix bug where the ID /GET request was cached and so two Peers created simultaneously would get the same ID: [See issue.](https://github.com/peers/peerjs-server/issues/2)
378 | - Add support for relative hostname. [See documentation.](https://github.com/peers/peerjs/blob/master/docs/api.md#new-peerid-options)
379 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Michelle Bu and Eric Zhang, http://peerjs.com
2 |
3 | (The MIT License)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PeerJS: Simple peer-to-peer with WebRTC
2 |
3 | [](#backers)
4 | [](#sponsors)
5 | [](https://discord.gg/Ud2PvAtK37)
6 |
7 | PeerJS provides a complete, configurable, and easy-to-use peer-to-peer API built on top of WebRTC, supporting both data channels and media streams.
8 |
9 | ## Live Example
10 |
11 | Here's an example application that uses both media and data connections: https://glitch.com/~peerjs-video. The example also uses its own [PeerServer](https://github.com/peers/peerjs-server).
12 |
13 | ---
14 |
15 |
16 | Special Announcement:
17 |
18 |
19 |
20 |
21 |
22 | We now have a Discord Channel
23 |
24 | There we plan to discuss roadmaps, feature requests, and moreJoin us today
25 |
26 |
27 | ---
28 |
29 | ## Setup
30 |
31 | **Include the library**
32 |
33 | with npm:
34 | `npm install peerjs`
35 |
36 | with yarn:
37 | `yarn add peerjs`
38 |
39 | ```js
40 | // The usage -
41 | import { Peer } from "peerjs";
42 | ```
43 |
44 | **Create a Peer**
45 |
46 | ```javascript
47 | const peer = new Peer("pick-an-id");
48 | // You can pick your own id or omit the id if you want to get a random one from the server.
49 | ```
50 |
51 | ## Data connections
52 |
53 | **Connect**
54 |
55 | ```javascript
56 | const conn = peer.connect("another-peers-id");
57 | conn.on("open", () => {
58 | conn.send("hi!");
59 | });
60 | ```
61 |
62 | **Receive**
63 |
64 | ```javascript
65 | peer.on("connection", (conn) => {
66 | conn.on("data", (data) => {
67 | // Will print 'hi!'
68 | console.log(data);
69 | });
70 | conn.on("open", () => {
71 | conn.send("hello!");
72 | });
73 | });
74 | ```
75 |
76 | ## Media calls
77 |
78 | **Call**
79 |
80 | ```javascript
81 | navigator.mediaDevices.getUserMedia(
82 | { video: true, audio: true },
83 | (stream) => {
84 | const call = peer.call("another-peers-id", stream);
85 | call.on("stream", (remoteStream) => {
86 | // Show stream in some element.
87 | });
88 | },
89 | (err) => {
90 | console.error("Failed to get local stream", err);
91 | },
92 | );
93 | ```
94 |
95 | **Answer**
96 |
97 | ```javascript
98 | peer.on("call", (call) => {
99 | navigator.mediaDevices.getUserMedia(
100 | { video: true, audio: true },
101 | (stream) => {
102 | call.answer(stream); // Answer the call with an A/V stream.
103 | call.on("stream", (remoteStream) => {
104 | // Show stream in some element.
105 | });
106 | },
107 | (err) => {
108 | console.error("Failed to get local stream", err);
109 | },
110 | );
111 | });
112 | ```
113 |
114 | ## Running tests
115 |
116 | ```bash
117 | npm test
118 | ```
119 |
120 | ## Browser support
121 |
122 | | [ ](http://godban.github.io/browsers-support-badges/) Firefox | [ ](http://godban.github.io/browsers-support-badges/) Chrome | [ ](http://godban.github.io/browsers-support-badges/) Edge | [ ](http://godban.github.io/browsers-support-badges/) Safari |
123 | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124 | | 80+ | 83+ | 83+ | 15+ |
125 |
126 | We test PeerJS against these versions of Chrome, Edge, Firefox, and Safari with [BrowserStack](https://www.browserstack.com) to ensure compatibility.
127 | It may work in other and older browsers, but we don't officially support them.
128 | Changes to browser support will be a breaking change going forward.
129 |
130 | > [!NOTE]
131 | > Firefox 102+ is required for CBOR / MessagePack support.
132 |
133 | ## FAQ
134 |
135 | Q. I have a message `Critical dependency: the request of a dependency is an expression` in browser's console
136 |
137 | A. The message occurs when you use PeerJS with Webpack. It is not critical! It relates to Parcel https://github.com/parcel-bundler/parcel/issues/2883 We'll resolve it when updated to Parcel V2.
138 |
139 | ## Links
140 |
141 | ### [Documentation / API Reference](https://peerjs.com/docs/)
142 |
143 | ### [PeerServer](https://github.com/peers/peerjs-server)
144 |
145 | ### [Discuss PeerJS on our Telegram Channel](https://t.me/joinchat/ENhPuhTvhm8WlIxTjQf7Og)
146 |
147 | ### [Changelog](https://github.com/peers/peerjs/blob/master/CHANGELOG.md)
148 |
149 | ## Contributors
150 |
151 | This project exists thanks to all the people who contribute.
152 |
153 |
154 | ## Backers
155 |
156 | Thank you to all our backers! [[Become a backer](https://opencollective.com/peer#backer)]
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 | ## Sponsors
261 |
262 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/peer#sponsor)]
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 | ## License
276 |
277 | PeerJS is licensed under the [MIT License](https://tldrlegal.com/l/mit).
278 |
--------------------------------------------------------------------------------
/__test__/faker.ts:
--------------------------------------------------------------------------------
1 | import { WebSocket } from "mock-socket";
2 | import "webrtc-adapter";
3 |
4 | const fakeGlobals = {
5 | WebSocket,
6 | MediaStream: class MediaStream {
7 | private readonly _tracks: MediaStreamTrack[] = [];
8 |
9 | constructor(tracks?: MediaStreamTrack[]) {
10 | if (tracks) {
11 | this._tracks = tracks;
12 | }
13 | }
14 |
15 | getTracks(): MediaStreamTrack[] {
16 | return this._tracks;
17 | }
18 |
19 | addTrack(track: MediaStreamTrack) {
20 | this._tracks.push(track);
21 | }
22 | },
23 | MediaStreamTrack: class MediaStreamTrack {
24 | kind: string;
25 | id: string;
26 |
27 | private static _idCounter = 0;
28 |
29 | constructor() {
30 | this.id = `track#${fakeGlobals.MediaStreamTrack._idCounter++}`;
31 | }
32 | },
33 | RTCPeerConnection: class RTCPeerConnection {
34 | private _senders: RTCRtpSender[] = [];
35 |
36 | close() {}
37 |
38 | addTrack(track: MediaStreamTrack, ..._stream: MediaStream[]): RTCRtpSender {
39 | const newSender = new RTCRtpSender();
40 | newSender.replaceTrack(track);
41 |
42 | this._senders.push(newSender);
43 |
44 | return newSender;
45 | }
46 |
47 | // removeTrack(_: RTCRtpSender): void { }
48 |
49 | getSenders(): RTCRtpSender[] {
50 | return this._senders;
51 | }
52 | },
53 | RTCRtpSender: class RTCRtpSender {
54 | readonly dtmf: RTCDTMFSender | null;
55 | readonly rtcpTransport: RTCDtlsTransport | null;
56 | track: MediaStreamTrack | null;
57 | readonly transport: RTCDtlsTransport | null;
58 |
59 | replaceTrack(withTrack: MediaStreamTrack | null): Promise {
60 | this.track = withTrack;
61 |
62 | return Promise.resolve();
63 | }
64 | },
65 | };
66 |
67 | Object.assign(global, fakeGlobals);
68 | Object.assign(window, fakeGlobals);
69 |
--------------------------------------------------------------------------------
/__test__/logger.spec.ts:
--------------------------------------------------------------------------------
1 | import Logger, { LogLevel } from "../lib/logger";
2 | import { expect, beforeAll, afterAll, describe, it } from "@jest/globals";
3 |
4 | describe("Logger", () => {
5 | let oldLoggerPrint;
6 | beforeAll(() => {
7 | //@ts-ignore
8 | oldLoggerPrint = Logger._print;
9 | });
10 |
11 | it("should be disabled by default", () => {
12 | expect(Logger.logLevel).toBe(LogLevel.Disabled);
13 | });
14 |
15 | it("should be accept new log level", () => {
16 | const checkedLevels = [];
17 |
18 | Logger.setLogFunction((logLevel) => {
19 | checkedLevels.push(logLevel);
20 | });
21 |
22 | Logger.logLevel = LogLevel.Warnings;
23 |
24 | expect(Logger.logLevel).toBe(LogLevel.Warnings);
25 |
26 | Logger.log("");
27 | Logger.warn("");
28 | Logger.error("");
29 |
30 | expect(checkedLevels).toEqual([LogLevel.Warnings, LogLevel.Errors]);
31 | });
32 |
33 | it("should accept new log function", () => {
34 | Logger.logLevel = LogLevel.All;
35 |
36 | const checkedLevels = [];
37 | const testMessage = "test it";
38 |
39 | Logger.setLogFunction((logLevel, ...args) => {
40 | checkedLevels.push(logLevel);
41 |
42 | expect(args[0]).toBe(testMessage);
43 | });
44 |
45 | Logger.log(testMessage);
46 | Logger.warn(testMessage);
47 | Logger.error(testMessage);
48 |
49 | expect(checkedLevels).toEqual([
50 | LogLevel.All,
51 | LogLevel.Warnings,
52 | LogLevel.Errors,
53 | ]);
54 | });
55 |
56 | afterAll(() => {
57 | Logger.setLogFunction(oldLoggerPrint);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/__test__/peer.spec.ts:
--------------------------------------------------------------------------------
1 | import "./setup";
2 | import { Peer } from "../lib/peer";
3 | import { Server } from "mock-socket";
4 | import { ConnectionType, PeerErrorType, ServerMessageType } from "../lib/enums";
5 | import { expect, beforeAll, afterAll, describe, it } from "@jest/globals";
6 |
7 | const createMockServer = (): Server => {
8 | const fakeURL = "ws://localhost:8080/peerjs?key=peerjs&id=1&token=testToken";
9 | const mockServer = new Server(fakeURL);
10 |
11 | mockServer.on("connection", (socket) => {
12 | //@ts-ignore
13 | socket.on("message", (data) => {
14 | socket.send("test message from mock server");
15 | });
16 |
17 | socket.send(JSON.stringify({ type: ServerMessageType.Open }));
18 | });
19 |
20 | return mockServer;
21 | };
22 | describe("Peer", () => {
23 | describe("after construct without parameters", () => {
24 | it("shouldn't contains any connection", () => {
25 | const peer = new Peer();
26 |
27 | expect(peer.open).toBe(false);
28 | expect(peer.connections).toEqual({});
29 | expect(peer.id).toBeNull();
30 | expect(peer.disconnected).toBe(false);
31 | expect(peer.destroyed).toBe(false);
32 |
33 | peer.destroy();
34 | });
35 | });
36 |
37 | describe("after construct with parameters", () => {
38 | it("should contains id and key", () => {
39 | const peer = new Peer("1", { key: "anotherKey" });
40 |
41 | expect(peer.id).toBe("1");
42 | expect(peer.options.key).toBe("anotherKey");
43 |
44 | peer.destroy();
45 | });
46 | });
47 |
48 | describe.skip("after call to peer #2", () => {
49 | let mockServer;
50 |
51 | beforeAll(() => {
52 | mockServer = createMockServer();
53 | });
54 |
55 | it("Peer#1 should has id #1", (done) => {
56 | const peer1 = new Peer("1", { port: 8080, host: "localhost" });
57 | expect(peer1.open).toBe(false);
58 |
59 | const mediaOptions = {
60 | metadata: { var: "123" },
61 | constraints: {
62 | mandatory: {
63 | OfferToReceiveAudio: true,
64 | OfferToReceiveVideo: true,
65 | },
66 | },
67 | };
68 |
69 | const track = new MediaStreamTrack();
70 | const mediaStream = new MediaStream([track]);
71 |
72 | const mediaConnection = peer1.call("2", mediaStream, { ...mediaOptions });
73 |
74 | expect(typeof mediaConnection.connectionId).toBe("string");
75 | expect(mediaConnection.type).toBe(ConnectionType.Media);
76 | expect(mediaConnection.peer).toBe("2");
77 | expect(mediaConnection.options).toEqual(
78 | // expect.arrayContaining([mediaOptions]),mediaOptions
79 | expect.objectContaining(mediaOptions),
80 | );
81 | expect(mediaConnection.metadata).toEqual(mediaOptions.metadata);
82 | expect(mediaConnection.peerConnection.getSenders()[0].track.id).toBe(
83 | track.id,
84 | );
85 |
86 | peer1.once("open", (id) => {
87 | expect(id).toBe("1");
88 | //@ts-ignore
89 | expect(peer1._lastServerId).toBe("1");
90 | expect(peer1.disconnected).toBe(false);
91 | expect(peer1.destroyed).toBe(false);
92 | expect(peer1.open).toBe(true);
93 |
94 | peer1.destroy();
95 |
96 | expect(peer1.disconnected).toBe(true);
97 | expect(peer1.destroyed).toBe(true);
98 | expect(peer1.open).toBe(false);
99 | expect(peer1.connections).toEqual({});
100 |
101 | done();
102 | });
103 | });
104 |
105 | afterAll(() => {
106 | mockServer.stop();
107 | });
108 | });
109 |
110 | describe("reconnect", () => {
111 | let mockServer;
112 |
113 | beforeAll(() => {
114 | mockServer = createMockServer();
115 | });
116 |
117 | it("connect to server => disconnect => reconnect => destroy", (done) => {
118 | const peer1 = new Peer("1", { port: 8080, host: "localhost" });
119 |
120 | peer1.once("open", () => {
121 | expect(peer1.open).toBe(true);
122 |
123 | peer1.once("disconnected", () => {
124 | expect(peer1.disconnected).toBe(true);
125 | expect(peer1.destroyed).toBe(false);
126 | expect(peer1.open).toBe(false);
127 |
128 | peer1.once("open", (id) => {
129 | expect(id).toBe("1");
130 | expect(peer1.disconnected).toBe(false);
131 | expect(peer1.destroyed).toBe(false);
132 | expect(peer1.open).toBe(true);
133 |
134 | peer1.once("disconnected", () => {
135 | expect(peer1.disconnected).toBe(true);
136 | expect(peer1.destroyed).toBe(false);
137 | expect(peer1.open).toBe(false);
138 |
139 | peer1.once("close", () => {
140 | expect(peer1.disconnected).toBe(true);
141 | expect(peer1.destroyed).toBe(true);
142 | expect(peer1.open).toBe(false);
143 |
144 | done();
145 | });
146 | });
147 |
148 | peer1.destroy();
149 | });
150 |
151 | peer1.reconnect();
152 | });
153 |
154 | peer1.disconnect();
155 | });
156 | });
157 |
158 | it("disconnect => reconnect => destroy", (done) => {
159 | mockServer.stop();
160 |
161 | const peer1 = new Peer("1", { port: 8080, host: "localhost" });
162 |
163 | peer1.once("disconnected", (id) => {
164 | expect(id).toBe("1");
165 | expect(peer1.disconnected).toBe(true);
166 | expect(peer1.destroyed).toBe(false);
167 | expect(peer1.open).toBe(false);
168 |
169 | peer1.once("open", (id) => {
170 | expect(id).toBe("1");
171 | expect(peer1.disconnected).toBe(false);
172 | expect(peer1.destroyed).toBe(false);
173 | expect(peer1.open).toBe(true);
174 |
175 | peer1.once("disconnected", () => {
176 | expect(peer1.disconnected).toBe(true);
177 | expect(peer1.destroyed).toBe(false);
178 | expect(peer1.open).toBe(false);
179 |
180 | peer1.once("close", () => {
181 | expect(peer1.disconnected).toBe(true);
182 | expect(peer1.destroyed).toBe(true);
183 | expect(peer1.open).toBe(false);
184 |
185 | done();
186 | });
187 | });
188 |
189 | peer1.destroy();
190 | });
191 |
192 | mockServer = createMockServer();
193 |
194 | peer1.reconnect();
195 | });
196 | });
197 |
198 | it("destroy peer if no id and no connection", (done) => {
199 | mockServer.stop();
200 |
201 | const peer1 = new Peer({ port: 8080, host: "localhost" });
202 |
203 | peer1.once("error", (error) => {
204 | expect(error.type).toBe(PeerErrorType.ServerError);
205 |
206 | peer1.once("close", () => {
207 | expect(peer1.disconnected).toBe(true);
208 | expect(peer1.destroyed).toBe(true);
209 | expect(peer1.open).toBe(false);
210 |
211 | done();
212 | });
213 |
214 | mockServer = createMockServer();
215 | });
216 | });
217 |
218 | afterAll(() => {
219 | mockServer.stop();
220 | });
221 | });
222 | });
223 |
--------------------------------------------------------------------------------
/__test__/setup.ts:
--------------------------------------------------------------------------------
1 | import "./faker";
2 | import { util } from "../lib/util";
3 |
4 | //enable support for WebRTC
5 | util.supports.audioVideo = true;
6 | util.randomToken = () => "testToken";
7 |
--------------------------------------------------------------------------------
/__test__/util.spec.ts:
--------------------------------------------------------------------------------
1 | import "./setup";
2 | import { util } from "../lib/util";
3 | import { expect, describe, it } from "@jest/globals";
4 |
5 | describe("util", () => {
6 | describe("#chunkedMTU", () => {
7 | it("should be 16300", () => {
8 | expect(util.chunkedMTU).toBe(16300);
9 | });
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/e2e/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/e2e/alice.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Alice
4 |
5 |
6 |
--------------------------------------------------------------------------------
/e2e/bob.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bob
4 |
5 |
6 |
--------------------------------------------------------------------------------
/e2e/data.js:
--------------------------------------------------------------------------------
1 | export const numbers = [
2 | 0,
3 | 1,
4 | -1,
5 | //
6 | Math.PI,
7 | -Math.PI,
8 | //8 bit
9 | 0x7f,
10 | 0x0f,
11 | //16 bit
12 | 0x7fff,
13 | 0x0fff,
14 | //32 bit
15 | 0x7fffffff,
16 | 0x0fffffff,
17 | //64 bit
18 | // 0x7FFFFFFFFFFFFFFF,
19 | // eslint-disable-next-line no-loss-of-precision
20 | 0x0fffffffffffffff,
21 | ];
22 | export const long_string = "ThisIsÁTèstString".repeat(100000);
23 | export const strings = [
24 | "",
25 | "hello",
26 | "café",
27 | "中文",
28 | "broccoli🥦līp𨋢grin😃ok",
29 | "\u{10ffff}",
30 | ];
31 |
32 | export { commit_data } from "./commit_data.js";
33 |
34 | export const uint8_arrays = [
35 | new Uint8Array(),
36 | new Uint8Array([0]),
37 | new Uint8Array([0, 1, 2, 3, 4, 6, 7]),
38 | new Uint8Array([0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15]),
39 | new Uint8Array([
40 | 0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23,
41 | 24, 25, 26, 27, 28, 30, 31,
42 | ]),
43 | ];
44 |
45 | export const int32_arrays = [
46 | new Int32Array([0].map((x) => -x)),
47 | new Int32Array([0, 1, 2, 3, 4, 6, 7].map((x) => -x)),
48 | new Int32Array(
49 | [0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15].map((x) => -x),
50 | ),
51 | new Int32Array(
52 | [
53 | 0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22,
54 | 23, 24, 25, 26, 27, 28, 30, 31,
55 | ].map((x) => -x),
56 | ),
57 | ];
58 |
59 | export const typed_array_view = new Uint8Array(uint8_arrays[4].buffer, 4);
60 |
61 | export const array_buffers = [
62 | new Uint8Array(),
63 | new Uint8Array([0]),
64 | new Uint8Array([0, 1, 2, 3, 4, 6, 7]),
65 | new Uint8Array([0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15]),
66 | new Uint8Array([
67 | 0, 1, 2, 3, 4, 6, 78, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23,
68 | 24, 25, 26, 27, 28, 30, 31,
69 | ]),
70 | ].map((x) => x.buffer);
71 |
72 | export const dates = [
73 | new Date(Date.UTC(2024, 1, 1, 1, 1, 1, 1)),
74 | new Date(Date.UTC(1, 1, 1, 1, 1, 1, 1)),
75 | ];
76 |
--------------------------------------------------------------------------------
/e2e/datachannel/Int32Array.js:
--------------------------------------------------------------------------------
1 | import { int32_arrays } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | for (const [i, typed_array] of int32_arrays.entries()) {
7 | expect(received[i]).to.be.an.instanceof(Int32Array);
8 | expect(received[i]).to.deep.equal(typed_array);
9 | }
10 | };
11 | /**
12 | * @param {import("../peerjs").DataConnection} dataConnection
13 | */
14 | export const send = (dataConnection) => {
15 | for (const typed_array of int32_arrays) {
16 | dataConnection.send(typed_array);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/Int32Array_as_ArrayBuffer.js:
--------------------------------------------------------------------------------
1 | import { int32_arrays } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | for (const [i, typed_array] of int32_arrays.entries()) {
7 | expect(received[i]).to.be.an.instanceof(ArrayBuffer);
8 | expect(new Int32Array(received[i])).to.deep.equal(typed_array);
9 | }
10 | };
11 | /**
12 | * @param {import("../peerjs").DataConnection} dataConnection
13 | */
14 | export const send = (dataConnection) => {
15 | for (const typed_array of int32_arrays) {
16 | dataConnection.send(typed_array);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/Int32Array_as_Uint8Array.js:
--------------------------------------------------------------------------------
1 | import { int32_arrays } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | for (const [i, typed_array] of int32_arrays.entries()) {
7 | expect(received[i]).to.be.an.instanceof(Uint8Array);
8 | expect(received[i]).to.deep.equal(new Uint8Array(typed_array.buffer));
9 | }
10 | };
11 | /**
12 | * @param {import("../peerjs").DataConnection} dataConnection
13 | */
14 | export const send = (dataConnection) => {
15 | for (const typed_array of int32_arrays) {
16 | dataConnection.send(typed_array);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/TypedArrayView_as_ArrayBuffer.js:
--------------------------------------------------------------------------------
1 | import { typed_array_view } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | const received_typed_array_view = received[0];
7 | expect(received_typed_array_view).to.be.instanceOf(ArrayBuffer);
8 | expect(new Uint8Array(received_typed_array_view)).to.deep.equal(
9 | typed_array_view,
10 | );
11 | };
12 | /**
13 | * @param {import("../peerjs").DataConnection} dataConnection
14 | */
15 | export const send = (dataConnection) => {
16 | dataConnection.send(typed_array_view);
17 | };
18 |
--------------------------------------------------------------------------------
/e2e/datachannel/Uint8Array.js:
--------------------------------------------------------------------------------
1 | import { uint8_arrays } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | for (const [i, typed_array] of uint8_arrays.entries()) {
7 | expect(received[i]).to.be.an.instanceof(Uint8Array);
8 | expect(received[i]).to.deep.equal(typed_array);
9 | }
10 | };
11 | /**
12 | * @param {import("../peerjs").DataConnection} dataConnection
13 | */
14 | export const send = (dataConnection) => {
15 | for (const typed_array of uint8_arrays) {
16 | dataConnection.send(typed_array);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/Uint8Array_as_ArrayBuffer.js:
--------------------------------------------------------------------------------
1 | import { uint8_arrays } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | for (const [i, typed_array] of uint8_arrays.entries()) {
7 | expect(received[i]).to.be.an.instanceof(ArrayBuffer);
8 | expect(new Uint8Array(received[i])).to.deep.equal(typed_array);
9 | }
10 | };
11 | /**
12 | * @param {import("../peerjs").DataConnection} dataConnection
13 | */
14 | export const send = (dataConnection) => {
15 | for (const typed_array of uint8_arrays) {
16 | dataConnection.send(typed_array);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/arraybuffers.js:
--------------------------------------------------------------------------------
1 | import { array_buffers } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | for (const [i, array_buffer] of array_buffers.entries()) {
7 | expect(received[i]).to.be.an.instanceof(ArrayBuffer);
8 | expect(received[i]).to.deep.equal(array_buffer);
9 | }
10 | };
11 | /**
12 | * @param {import("../peerjs").DataConnection} dataConnection
13 | */
14 | export const send = (dataConnection) => {
15 | for (const array_buffer of array_buffers) {
16 | dataConnection.send(array_buffer);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/arraybuffers_as_uint8array.js:
--------------------------------------------------------------------------------
1 | import { array_buffers } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | for (const [i, array_buffer] of array_buffers.entries()) {
7 | expect(received[i]).to.be.an.instanceof(Uint8Array);
8 | expect(received[i]).to.deep.equal(new Uint8Array(array_buffer));
9 | }
10 | };
11 | /**
12 | * @param {import("../peerjs").DataConnection} dataConnection
13 | */
14 | export const send = (dataConnection) => {
15 | for (const array_buffer of array_buffers) {
16 | dataConnection.send(array_buffer);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/arrays.js:
--------------------------------------------------------------------------------
1 | import { commit_data } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | expect(received[1]).to.be.an("array").with.lengthOf(commit_data.length);
7 | expect(received).to.deep.equal([[], commit_data]);
8 | };
9 | /**
10 | * @param {import("../peerjs").DataConnection} dataConnection
11 | */
12 | export const send = (dataConnection) => {
13 | dataConnection.send([]);
14 | dataConnection.send(commit_data);
15 | };
16 |
--------------------------------------------------------------------------------
/e2e/datachannel/arrays_unchunked.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JSON transfer does not chunk, commit_data is too large to send
3 | */
4 |
5 | import { commit_data } from "../data.js";
6 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
7 |
8 | /** @param {unknown[]} received */
9 | export const check = (received) => {
10 | expect(received).to.deep.equal([[], commit_data.slice(0, 2)]);
11 | };
12 | /**
13 | * @param {import("../peerjs").DataConnection} dataConnection
14 | */
15 | export const send = (dataConnection) => {
16 | dataConnection.send([]);
17 | dataConnection.send(commit_data.slice(0, 2));
18 | };
19 |
--------------------------------------------------------------------------------
/e2e/datachannel/blobs.js:
--------------------------------------------------------------------------------
1 | import { commit_data } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | const Encoder = new TextEncoder();
5 | const Decoder = new TextDecoder();
6 |
7 | /** @param {unknown[]} received */
8 | export const check = (received) => {
9 | expect(received).to.be.an("array").with.lengthOf(commit_data.length);
10 | const commits_as_arraybuffer = commit_data.map(
11 | (blob) => Encoder.encode(JSON.stringify(blob)).buffer,
12 | );
13 | expect(received).to.deep.equal(commits_as_arraybuffer);
14 | const parsed_received = received.map((r) => JSON.parse(Decoder.decode(r)));
15 | expect(parsed_received).to.deep.equal(commit_data);
16 | };
17 | /**
18 | * @param {import("../peerjs").DataConnection} dataConnection
19 | */
20 | export const send = async (dataConnection) => {
21 | for (const commit of commit_data) {
22 | await dataConnection.send(new Blob([JSON.stringify(commit)]));
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/e2e/datachannel/dates.js:
--------------------------------------------------------------------------------
1 | import { dates } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | expect(received).to.deep.equal(dates);
7 | };
8 | /**
9 | * @param {import("../peerjs").DataConnection} dataConnection
10 | */
11 | export const send = (dataConnection) => {
12 | for (const date of dates) {
13 | dataConnection.send(date);
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/e2e/datachannel/dates_as_json_string.js:
--------------------------------------------------------------------------------
1 | import { dates } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | console.log(dates.map((date) => date.toString()));
7 | expect(received).to.deep.equal(dates.map((date) => date.toJSON()));
8 | };
9 | /**
10 | * @param {import("../peerjs").DataConnection} dataConnection
11 | */
12 | export const send = (dataConnection) => {
13 | for (const date of dates) {
14 | dataConnection.send(date);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/e2e/datachannel/dates_as_string.js:
--------------------------------------------------------------------------------
1 | import { dates } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | console.log(dates.map((date) => date.toString()));
7 | expect(received).to.deep.equal(dates.map((date) => date.toString()));
8 | };
9 | /**
10 | * @param {import("../peerjs").DataConnection} dataConnection
11 | */
12 | export const send = (dataConnection) => {
13 | for (const date of dates) {
14 | dataConnection.send(date);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/e2e/datachannel/files.js:
--------------------------------------------------------------------------------
1 | import { commit_data } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | const Encoder = new TextEncoder();
5 |
6 | /** @param {unknown[]} received */
7 | export const check = (received) => {
8 | expect(received).to.be.an("array").with.lengthOf(commit_data.length);
9 | const commits_as_arraybuffer = commit_data.map(
10 | (blob) => Encoder.encode(JSON.stringify(blob)).buffer,
11 | );
12 | expect(received).to.deep.equal(commits_as_arraybuffer);
13 | };
14 | /**
15 | * @param {import("../peerjs").DataConnection} dataConnection
16 | */
17 | export const send = async (dataConnection) => {
18 | for (const commit of commit_data) {
19 | await dataConnection.send(
20 | new File([JSON.stringify(commit)], "commit.txt", {
21 | type: "dummy/data",
22 | }),
23 | );
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/e2e/datachannel/long_string.js:
--------------------------------------------------------------------------------
1 | import { long_string } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | expect(received).to.deep.equal([long_string]);
7 | };
8 | /**
9 | * @param {import("../peerjs").DataConnection} dataConnection
10 | */
11 | export const send = (dataConnection) => {
12 | dataConnection.send(long_string);
13 | };
14 |
--------------------------------------------------------------------------------
/e2e/datachannel/numbers.js:
--------------------------------------------------------------------------------
1 | import { numbers } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | expect(received).to.deep.equal(numbers);
7 | };
8 | /**
9 | * @param {import("../peerjs").DataConnection} dataConnection
10 | */
11 | export const send = (dataConnection) => {
12 | for (const number of numbers) {
13 | dataConnection.send(number);
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/e2e/datachannel/objects.js:
--------------------------------------------------------------------------------
1 | import { commit_data } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | expect(received).to.be.an("array").with.lengthOf(commit_data.length);
7 | expect(received).to.deep.equal(commit_data);
8 | };
9 | /**
10 | * @param {import("../peerjs").DataConnection} dataConnection
11 | */
12 | export const send = (dataConnection) => {
13 | for (const commit of commit_data) {
14 | dataConnection.send(commit);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/e2e/datachannel/serialization.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Serialization
11 |
12 |
13 | Connect
14 | Send
15 | Check
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/e2e/datachannel/serialization.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {typeof import("../../lib/exports.js").Peer}
3 | */
4 | const Peer = window.peerjs.Peer;
5 |
6 | const params = new URLSearchParams(document.location.search);
7 | const testfile = params.get("testfile");
8 | const serialization = params.get("serialization");
9 |
10 | (async () => {
11 | let serializers = {};
12 | try {
13 | const { MsgPack } = await import("/dist/serializer.msgpack.mjs");
14 | serializers = {
15 | MsgPack,
16 | };
17 | } catch (e) {
18 | console.log(e);
19 | }
20 |
21 | const { check, send } = await import(`./${testfile}.js`);
22 | document.getElementsByTagName("title")[0].innerText =
23 | window.location.hash.substring(1);
24 |
25 | const checkBtn = document.getElementById("check-btn");
26 | const sendBtn = document.getElementById("send-btn");
27 | const receiverIdInput = document.getElementById("receiver-id");
28 | const connectBtn = document.getElementById("connect-btn");
29 | const messages = document.getElementById("messages");
30 | const result = document.getElementById("result");
31 | const errorMessage = document.getElementById("error-message");
32 |
33 | const peer = new Peer({
34 | debug: 3,
35 | serializers,
36 | key: params.get("key"),
37 | });
38 | const received = [];
39 | /**
40 | * @type {import("../../lib/exports.js").DataConnection}
41 | */
42 | let dataConnection;
43 | peer
44 | .once("open", (id) => {
45 | messages.textContent = `Your Peer ID: ${id}`;
46 | })
47 | .once("error", (error) => {
48 | errorMessage.textContent = JSON.stringify(error);
49 | })
50 | .once("connection", (connection) => {
51 | dataConnection = connection;
52 | dataConnection.on("data", (data) => {
53 | console.log(data);
54 | received.push(data);
55 | });
56 | dataConnection.once("close", () => {
57 | messages.textContent = "Closed!";
58 | });
59 | });
60 |
61 | connectBtn.addEventListener("click", () => {
62 | const receiverId = receiverIdInput.value;
63 | if (receiverId) {
64 | dataConnection = peer.connect(receiverId, {
65 | reliable: true,
66 | serialization,
67 | });
68 | dataConnection.once("open", () => {
69 | messages.textContent = "Connected!";
70 | });
71 | }
72 | });
73 |
74 | checkBtn.addEventListener("click", async () => {
75 | try {
76 | console.log(received);
77 | check(received);
78 | result.textContent = "Success!";
79 | } catch (e) {
80 | result.textContent = "Failed!";
81 | errorMessage.textContent = JSON.stringify(e.message);
82 | } finally {
83 | messages.textContent = "Checked!";
84 | }
85 | });
86 |
87 | sendBtn.addEventListener("click", async () => {
88 | dataConnection.once("error", (err) => {
89 | errorMessage.innerText = err.toString();
90 | });
91 | await send(dataConnection);
92 | dataConnection.close({ flush: true });
93 | messages.textContent = "Sent!";
94 | });
95 | window["connect-btn"].disabled = false;
96 | })();
97 |
--------------------------------------------------------------------------------
/e2e/datachannel/serialization.page.ts:
--------------------------------------------------------------------------------
1 | import { browser, $ } from "@wdio/globals";
2 |
3 | const { BYPASS_WAF } = process.env;
4 |
5 | class SerializationPage {
6 | get sendBtn() {
7 | return $("button[id='send-btn']");
8 | }
9 |
10 | get checkBtn() {
11 | return $("button[id='check-btn']");
12 | }
13 | get connectBtn() {
14 | return $("button[id='connect-btn']");
15 | }
16 |
17 | get receiverId() {
18 | return $("input[id='receiver-id']");
19 | }
20 |
21 | get messages() {
22 | return $("div[id='messages']");
23 | }
24 |
25 | get errorMessage() {
26 | return $("div[id='error-message']");
27 | }
28 |
29 | get result() {
30 | return $("div[id='result']");
31 | }
32 |
33 | waitForMessage(right: string) {
34 | return browser.waitUntil(
35 | async () => {
36 | const messages = await this.messages.getText();
37 | return messages.startsWith(right);
38 | },
39 | { timeoutMsg: `Expected message to start with ${right}`, timeout: 10000 },
40 | );
41 | }
42 |
43 | async open(testFile: string, serialization: string) {
44 | await browser.switchWindow("Alice");
45 | await browser.url(
46 | `/e2e/datachannel/serialization.html?testfile=${testFile}&serialization=${serialization}&key=${BYPASS_WAF}#Alice`,
47 | );
48 | await this.connectBtn.waitForEnabled();
49 |
50 | await browser.switchWindow("Bob");
51 | await browser.url(
52 | `/e2e/datachannel/serialization.html?testfile=${testFile}&key=${BYPASS_WAF}#Bob`,
53 | );
54 | await this.connectBtn.waitForEnabled();
55 | }
56 | async init() {
57 | await browser.url("/e2e/alice.html");
58 | await browser.waitUntil(async () => {
59 | const title = await browser.getTitle();
60 | return title === "Alice";
61 | });
62 | await browser.newWindow("/e2e/bob.html");
63 | await browser.waitUntil(async () => {
64 | const title = await browser.getTitle();
65 | return title === "Bob";
66 | });
67 | }
68 | }
69 |
70 | export default new SerializationPage();
71 |
--------------------------------------------------------------------------------
/e2e/datachannel/serializationTest.ts:
--------------------------------------------------------------------------------
1 | import P from "./serialization.page.js";
2 | import { browser, expect } from "@wdio/globals";
3 |
4 | export const serializationTest =
5 | (testFile: string, serialization: string) => async () => {
6 | await P.open(testFile, serialization);
7 | await P.waitForMessage("Your Peer ID: ");
8 | const bobId = (await P.messages.getText()).split("Your Peer ID: ")[1];
9 | await browser.switchWindow("Alice");
10 | await P.waitForMessage("Your Peer ID: ");
11 | await P.receiverId.setValue(bobId);
12 | await P.connectBtn.click();
13 | await P.waitForMessage("Connected!");
14 | await P.sendBtn.click();
15 | await P.waitForMessage("Sent!");
16 | await browser.switchWindow("Bob");
17 | await P.waitForMessage("Closed!");
18 | await P.checkBtn.click();
19 | await P.waitForMessage("Checked!");
20 |
21 | await expect(await P.errorMessage.getText()).toBe("");
22 | await expect(await P.result.getText()).toBe("Success!");
23 | };
24 |
--------------------------------------------------------------------------------
/e2e/datachannel/serialization_binary.spec.ts:
--------------------------------------------------------------------------------
1 | import P from "./serialization.page.js";
2 | import { serializationTest } from "./serializationTest.js";
3 | describe("DataChannel:Binary", () => {
4 | beforeAll(
5 | async () => {
6 | await P.init();
7 | },
8 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
9 | 2,
10 | );
11 | it(
12 | "should transfer numbers",
13 | serializationTest("./numbers", "binary"),
14 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
15 | 2,
16 | );
17 | it(
18 | "should transfer strings",
19 | serializationTest("./strings", "binary"),
20 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
21 | 2,
22 | );
23 | it(
24 | "should transfer long string",
25 | serializationTest("./long_string", "binary"),
26 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
27 | 2,
28 | );
29 | it(
30 | "should transfer objects",
31 | serializationTest("./objects", "binary"),
32 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
33 | 2,
34 | );
35 | it(
36 | "should transfer Blobs",
37 | serializationTest("./blobs", "binary"),
38 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
39 | 2,
40 | );
41 | it(
42 | "should transfer Files",
43 | serializationTest("./files", "binary"),
44 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
45 | 2,
46 | );
47 | it(
48 | "should transfer arrays",
49 | serializationTest("./arrays", "binary"),
50 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
51 | 2,
52 | );
53 | it(
54 | "should transfer Dates as strings",
55 | serializationTest("./dates_as_string", "binary"),
56 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
57 | 2,
58 | );
59 | it(
60 | "should transfer ArrayBuffers",
61 | serializationTest("./arraybuffers", "binary"),
62 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
63 | 2,
64 | );
65 | it(
66 | "should transfer TypedArrayView as ArrayBuffer",
67 | serializationTest("./TypedArrayView_as_ArrayBuffer", "binary"),
68 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
69 | 2,
70 | );
71 | it(
72 | "should transfer Uint8Arrays as ArrayBuffer",
73 | serializationTest("./Uint8Array_as_ArrayBuffer", "binary"),
74 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
75 | 2,
76 | );
77 | it(
78 | "should transfer Int32Arrays as ArrayBuffer",
79 | serializationTest("./Int32Array_as_ArrayBuffer", "binary"),
80 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
81 | 2,
82 | );
83 | });
84 |
--------------------------------------------------------------------------------
/e2e/datachannel/serialization_json.spec.ts:
--------------------------------------------------------------------------------
1 | import P from "./serialization.page.js";
2 | import { serializationTest } from "./serializationTest.js";
3 |
4 | describe("DataChannel:JSON", () => {
5 | beforeAll(
6 | async () => {
7 | await P.init();
8 | },
9 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
10 | 2,
11 | );
12 | it(
13 | "should transfer numbers",
14 | serializationTest("./numbers", "json"),
15 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
16 | 2,
17 | );
18 | it(
19 | "should transfer strings",
20 | serializationTest("./strings", "json"),
21 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
22 | 2,
23 | );
24 | // it("should transfer long string", serializationTest("./long_string", "json"));
25 | it(
26 | "should transfer objects",
27 | serializationTest("./objects", "json"),
28 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
29 | 2,
30 | );
31 | it(
32 | "should transfer arrays (no chunks)",
33 | serializationTest("./arrays_unchunked", "json"),
34 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
35 | 2,
36 | );
37 | it(
38 | "should transfer Dates as strings",
39 | serializationTest("./dates_as_json_string", "json"),
40 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
41 | 2,
42 | );
43 | // it("should transfer ArrayBuffers", serializationTest("./arraybuffers", "json"));
44 | // it("should transfer TypedArrayView", serializationTest("./typed_array_view", "json"));
45 | // it(
46 | // "should transfer Uint8Arrays",
47 | // serializationTest("./Uint8Array", "json"),
48 | // );
49 | // it(
50 | // "should transfer Int32Arrays",
51 | // serializationTest("./Int32Array", "json"),
52 | // );
53 | });
54 |
--------------------------------------------------------------------------------
/e2e/datachannel/serialization_msgpack.spec.ts:
--------------------------------------------------------------------------------
1 | import P from "./serialization.page.js";
2 | import { serializationTest } from "./serializationTest.js";
3 | import { browser } from "@wdio/globals";
4 |
5 | describe("DataChannel:MsgPack", function () {
6 | beforeAll(async function () {
7 | await P.init();
8 | });
9 | beforeEach(async function () {
10 | if (
11 | // @ts-ignore
12 | browser.capabilities.browserName === "firefox" &&
13 | // @ts-ignore
14 | parseInt(browser.capabilities.browserVersion) < 102
15 | ) {
16 | pending("Firefox 102+ required for Streams");
17 | }
18 | });
19 | it("should transfer numbers", serializationTest("./numbers", "MsgPack"));
20 | it("should transfer strings", serializationTest("./strings", "MsgPack"));
21 | it(
22 | "should transfer long string",
23 | serializationTest("./long_string", "MsgPack"),
24 | );
25 | it("should transfer objects", serializationTest("./objects", "MsgPack"));
26 | it("should transfer arrays", serializationTest("./arrays", "MsgPack"));
27 | it(
28 | "should transfer Dates as strings",
29 | serializationTest("./dates", "MsgPack"),
30 | );
31 | // it("should transfer ArrayBuffers", serializationTest("./arraybuffers", "MsgPack"));
32 | it(
33 | "should transfer TypedArrayView",
34 | serializationTest("./typed_array_view", "MsgPack"),
35 | );
36 | it(
37 | "should transfer Uint8Arrays",
38 | serializationTest("./Uint8Array", "MsgPack"),
39 | );
40 | it(
41 | "should transfer Int32Arrays as Uint8Arrays",
42 | serializationTest("./Int32Array_as_Uint8Array", "MsgPack"),
43 | );
44 | });
45 |
--------------------------------------------------------------------------------
/e2e/datachannel/strings.js:
--------------------------------------------------------------------------------
1 | import { strings } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | expect(received).to.deep.equal(strings);
7 | };
8 | /**
9 | * @param {import("../peerjs").DataConnection} dataConnection
10 | */
11 | export const send = (dataConnection) => {
12 | for (const string of strings) {
13 | dataConnection.send(string);
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/e2e/datachannel/typed_array_view.js:
--------------------------------------------------------------------------------
1 | import { typed_array_view } from "../data.js";
2 | import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
3 |
4 | /** @param {unknown[]} received */
5 | export const check = (received) => {
6 | const received_typed_array_view = received[0];
7 | expect(received_typed_array_view).to.deep.equal(typed_array_view);
8 | for (const [i, v] of received_typed_array_view.entries()) {
9 | expect(v).to.deep.equal(typed_array_view[i]);
10 | }
11 | };
12 | /**
13 | * @param {import("../peerjs").DataConnection} dataConnection
14 | */
15 | export const send = (dataConnection) => {
16 | dataConnection.send(typed_array_view);
17 | };
18 |
--------------------------------------------------------------------------------
/e2e/mediachannel/close.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | MediaChannel
11 |
12 |
13 |
26 |
27 |
28 | Call
29 | Hang up
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/e2e/mediachannel/close.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {typeof import("../..").Peer}
3 | */
4 | const Peer = window.peerjs.Peer;
5 |
6 | const params = new URLSearchParams(document.location.search);
7 |
8 | document.getElementsByTagName("title")[0].innerText =
9 | window.location.hash.substring(1);
10 |
11 | const callBtn = document.getElementById("call-btn");
12 | console.log(callBtn);
13 | const receiverIdInput = document.getElementById("receiver-id");
14 | const closeBtn = document.getElementById("close-btn");
15 | const messages = document.getElementById("messages");
16 | const errorMessage = document.getElementById("error-message");
17 |
18 | const stream = window["sender-stream"].captureStream(30);
19 | const peer = new Peer({ debug: 3, key: params.get("key") });
20 | /**
21 | * @type {import("peerjs").MediaConnection}
22 | */
23 | let mediaConnection;
24 | peer
25 | .once("open", (id) => {
26 | messages.textContent = `Your Peer ID: ${id}`;
27 | })
28 | .once("error", (error) => {
29 | errorMessage.textContent = JSON.stringify(error);
30 | })
31 | .once("call", (call) => {
32 | mediaConnection = call;
33 | mediaConnection
34 | .once("stream", function (stream) {
35 | const video = document.getElementById("receiver-stream");
36 | video.srcObject = stream;
37 | video.play();
38 | })
39 | .once("close", () => {
40 | messages.textContent = "Closed!";
41 | })
42 | .once("willCloseOnRemote", () => {
43 | messages.textContent = "Connected!";
44 | });
45 | call.answer(stream);
46 | });
47 |
48 | callBtn.addEventListener("click", async () => {
49 | console.log("calling");
50 |
51 | /** @type {string} */
52 | const receiverId = receiverIdInput.value;
53 | if (receiverId) {
54 | mediaConnection = peer.call(receiverId, stream);
55 | mediaConnection
56 | .once("stream", (stream) => {
57 | const video = document.getElementById("receiver-stream");
58 | video.srcObject = stream;
59 | video.play();
60 | messages.innerText = "Connected!";
61 | })
62 | .once("close", () => {
63 | messages.textContent = "Closed!";
64 | });
65 | }
66 | });
67 |
68 | closeBtn.addEventListener("click", () => {
69 | mediaConnection.close();
70 | });
71 |
72 | callBtn.disabled = false;
73 |
--------------------------------------------------------------------------------
/e2e/mediachannel/close.page.ts:
--------------------------------------------------------------------------------
1 | import { browser, $ } from "@wdio/globals";
2 |
3 | const { BYPASS_WAF } = process.env;
4 |
5 | class SerializationPage {
6 | get receiverId() {
7 | return $("input[id='receiver-id']");
8 | }
9 | get callBtn() {
10 | return $("button[id='call-btn']");
11 | }
12 |
13 | get messages() {
14 | return $("div[id='messages']");
15 | }
16 |
17 | get closeBtn() {
18 | return $("button[id='close-btn']");
19 | }
20 |
21 | get errorMessage() {
22 | return $("div[id='error-message']");
23 | }
24 |
25 | get result() {
26 | return $("div[id='result']");
27 | }
28 |
29 | waitForMessage(right: string) {
30 | return browser.waitUntil(
31 | async () => {
32 | const messages = await this.messages.getText();
33 | return messages.startsWith(right);
34 | },
35 | { timeoutMsg: `Expected message to start with ${right}`, timeout: 10000 },
36 | );
37 | }
38 |
39 | async open() {
40 | await browser.switchWindow("Alice");
41 | await browser.url(`/e2e/mediachannel/close.html?key=${BYPASS_WAF}#Alice`);
42 | await this.callBtn.waitForEnabled();
43 |
44 | await browser.switchWindow("Bob");
45 | await browser.url(`/e2e/mediachannel/close.html?key=${BYPASS_WAF}#Bob`);
46 | await this.callBtn.waitForEnabled();
47 | }
48 | async init() {
49 | await browser.url("/e2e/alice.html");
50 | await browser.waitUntil(async () => {
51 | const title = await browser.getTitle();
52 | return title === "Alice";
53 | });
54 | await browser.pause(1000);
55 | await browser.newWindow("/e2e/bob.html");
56 | await browser.waitUntil(async () => {
57 | const title = await browser.getTitle();
58 | return title === "Bob";
59 | });
60 | await browser.pause(1000);
61 | }
62 | }
63 |
64 | export default new SerializationPage();
65 |
--------------------------------------------------------------------------------
/e2e/mediachannel/close.spec.ts:
--------------------------------------------------------------------------------
1 | import P from "./close.page.js";
2 | import { browser } from "@wdio/globals";
3 |
4 | describe("MediaStream", () => {
5 | beforeAll(
6 | async () => {
7 | await P.init();
8 | },
9 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
10 | 1,
11 | );
12 | it(
13 | "should close the remote stream",
14 | async () => {
15 | await P.open();
16 | await P.waitForMessage("Your Peer ID: ");
17 | const bobId = (await P.messages.getText()).split("Your Peer ID: ")[1];
18 | await browser.switchWindow("Alice");
19 | await P.waitForMessage("Your Peer ID: ");
20 | await P.receiverId.setValue(bobId);
21 | await P.callBtn.click();
22 | await P.waitForMessage("Connected!");
23 | await browser.switchWindow("Bob");
24 | await P.waitForMessage("Connected!");
25 | await P.closeBtn.click();
26 | await P.waitForMessage("Closed!");
27 | await browser.switchWindow("Alice");
28 | await P.waitForMessage("Closed!");
29 | },
30 | jasmine.DEFAULT_TIMEOUT_INTERVAL,
31 | 2,
32 | );
33 | });
34 |
--------------------------------------------------------------------------------
/e2e/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/e2e/peer/disconnected.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ID-TAKEN
11 |
12 |
13 |
14 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/e2e/peer/id-taken.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ID-TAKEN
11 |
12 |
13 |
14 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/e2e/peer/peer.page.ts:
--------------------------------------------------------------------------------
1 | import { browser, $ } from "@wdio/globals";
2 |
3 | const { BYPASS_WAF } = process.env;
4 |
5 | class PeerPage {
6 | get messages() {
7 | return $("div[id='messages']");
8 | }
9 |
10 | get errorMessage() {
11 | return $("div[id='error-message']");
12 | }
13 |
14 | waitForMessage(right: string) {
15 | return browser.waitUntil(
16 | async () => {
17 | const messages = await this.messages.getText();
18 | return messages.startsWith(right);
19 | },
20 | { timeoutMsg: `Expected message to start with ${right}`, timeout: 10000 },
21 | );
22 | }
23 |
24 | async open(test: string) {
25 | await browser.url(`/e2e/peer/${test}.html?key=${BYPASS_WAF}`);
26 | }
27 | }
28 |
29 | export default new PeerPage();
30 |
--------------------------------------------------------------------------------
/e2e/peer/peer.spec.ts:
--------------------------------------------------------------------------------
1 | import P from "./peer.page.js";
2 | import { browser, expect } from "@wdio/globals";
3 |
4 | describe("Peer", () => {
5 | it("should emit an error, when the ID is already taken", async () => {
6 | await P.open("id-taken");
7 | await P.waitForMessage("No ID takeover");
8 | expect(await P.errorMessage.getText()).toBe("");
9 | });
10 | it("should emit an error, when the server is unavailable", async () => {
11 | await P.open("server-unavailable");
12 | await P.waitForMessage('{"type":"server-error"}');
13 | expect(await P.errorMessage.getText()).toBe("");
14 | });
15 | it("should emit an error, when it got disconnected from the server", async () => {
16 | await P.open("disconnected");
17 | await P.waitForMessage('{"type":"disconnected"}');
18 | expect(await P.errorMessage.getText()).toBe("");
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/e2e/peer/server-unavailable.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ID-TAKEN
11 |
12 |
13 |
14 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/e2e/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, sans-serif;
3 | max-width: 800px;
4 | margin: 0 auto;
5 | padding: 20px;
6 | }
7 |
8 | #inputs {
9 | display: flex;
10 | flex-wrap: wrap;
11 | gap: 10px;
12 | align-items: center;
13 | }
14 |
15 | button {
16 | background-color: #007bff;
17 | color: white;
18 | border: none;
19 | padding: 8px 16px;
20 | border-radius: 4px;
21 | cursor: pointer;
22 | }
23 |
24 | button:hover {
25 | background-color: #0056b3;
26 | }
27 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 | "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | // "outDir": "./", /* Redirect output structure to the directory. */
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true /* Enable all strict type-checking options. */,
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
43 |
44 | /* Module Resolution Options */
45 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
49 | // "typeRoots": [], /* List of folders to include type definitions from. */
50 | "types": [
51 | "node",
52 | "@wdio/globals/types",
53 | "jasmine",
54 | "@wdio/jasmine-framework"
55 | ],
56 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
57 | // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
59 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
60 |
61 | /* Source Map Options */
62 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
64 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
65 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
66 |
67 | /* Experimental Options */
68 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
69 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
70 |
71 | /* Advanced Options */
72 | "skipLibCheck": true /* Skip type checking of declaration files. */,
73 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/e2e/types.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: 0 */
2 |
3 | declare module "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs" {
4 | export = chai;
5 | }
6 |
7 | interface Window {
8 | "connect-btn": HTMLButtonElement;
9 | send: (dataConnection: import("../../peerjs").DataConnection) => void;
10 | check: (received: unknown[]) => void;
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/wdio.bstack.conf.ts:
--------------------------------------------------------------------------------
1 | import { config as sharedConfig } from "./wdio.shared.conf.js";
2 |
3 | export const config: WebdriverIO.Config = {
4 | ...sharedConfig,
5 | ...{
6 | maxInstances: 5,
7 | user: process.env.BROWSERSTACK_USERNAME,
8 | key: process.env.BROWSERSTACK_ACCESS_KEY,
9 | hostname: "hub.browserstack.com",
10 | services: [
11 | [
12 | "browserstack",
13 | {
14 | browserstackLocal: true,
15 | },
16 | ],
17 | ],
18 | capabilities: [
19 | {
20 | browserName: "Edge",
21 | "bstack:options": {
22 | os: "Windows",
23 | osVersion: "11",
24 | browserVersion: "83",
25 | localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
26 | },
27 | },
28 | {
29 | browserName: "Chrome",
30 | "bstack:options": {
31 | os: "Windows",
32 | osVersion: "11",
33 | browserVersion: "83",
34 | localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
35 | },
36 | },
37 | {
38 | browserName: "Chrome",
39 | "bstack:options": {
40 | browserVersion: "latest",
41 | os: "Windows",
42 | osVersion: "11",
43 | localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
44 | },
45 | },
46 | {
47 | browserName: "Firefox",
48 | "bstack:options": {
49 | os: "Windows",
50 | osVersion: "7",
51 | browserVersion: "80.0",
52 | localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
53 | },
54 | },
55 | {
56 | browserName: "Firefox",
57 | "bstack:options": {
58 | browserVersion: "105",
59 | os: "OS X",
60 | osVersion: "Ventura",
61 | localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
62 | },
63 | },
64 | // {
65 | // browserName: "Safari",
66 | // "bstack:options": {
67 | // browserVersion: "latest",
68 | // os: "OS X",
69 | // osVersion: "Monterey",
70 | // localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
71 | // },
72 | // },
73 | // {
74 | // browserName: 'Safari',
75 | // 'bstack:options': {
76 | // browserVersion: 'latest',
77 | // os: 'OS X',
78 | // osVersion: 'Ventura',
79 | // localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
80 | // }
81 | // },
82 | ],
83 | },
84 | };
85 |
--------------------------------------------------------------------------------
/e2e/wdio.local.conf.ts:
--------------------------------------------------------------------------------
1 | import { config as sharedConfig } from "./wdio.shared.conf.js";
2 |
3 | export const config: WebdriverIO.Config = {
4 | runner: "local",
5 | ...sharedConfig,
6 | services: ["geckodriver"],
7 | ...{
8 | capabilities: [
9 | {
10 | browserName: "chrome",
11 | "goog:chromeOptions": {
12 | args: ["headless", "disable-gpu"],
13 | },
14 | },
15 | {
16 | browserName: "firefox",
17 | "moz:firefoxOptions": {
18 | args: ["-headless"],
19 | },
20 | },
21 | ],
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/e2e/wdio.shared.conf.ts:
--------------------------------------------------------------------------------
1 | import url from "node:url";
2 | import path from "node:path";
3 |
4 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
5 |
6 | export const config: Omit = {
7 | injectGlobals: false,
8 | //
9 | // ====================
10 | // Runner Configuration
11 | // ====================
12 | //
13 | // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or
14 | // on a remote machine).
15 | // runner: 'local',
16 | //
17 | // ==================
18 | // Specify Test Files
19 | // ==================
20 | // Define which test specs should run. The pattern is relative to the directory
21 | // from which `wdio` was called. Notice that, if you are calling `wdio` from an
22 | // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
23 | // directory is where your package.json resides, so `wdio` will be called from there.
24 | //
25 | specs: ["./**/*.spec.ts"],
26 | // Patterns to exclude.
27 | exclude: [
28 | // 'path/to/excluded/files'
29 | ],
30 | //
31 | // ============
32 | // Capabilities
33 | // ============
34 | // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
35 | // time. Depending on the number of capabilities, WebdriverIO launches several test
36 | // sessions. Within your capabilities you can overwrite the spec and exclude options in
37 | // order to group specific specs to a specific capability.
38 | //
39 | // First, you can define how many instances should be started at the same time. Let's
40 | // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
41 | // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
42 | // files and you set maxInstances to 10, all spec files will get tested at the same time
43 | // and 30 processes will get spawned. The property handles how many capabilities
44 | // from the same test should run tests.
45 | //
46 | maxInstances: 5,
47 | //
48 | // ===================
49 | // Test Configurations
50 | // ===================
51 | // Define all options that are relevant for the WebdriverIO instance here
52 | //
53 | // Level of logging verbosity: trace | debug | info | warn | error | silent
54 | logLevel: "trace",
55 | outputDir: path.resolve(__dirname, "logs"),
56 | //
57 | // Set specific log levels per logger
58 | // loggers:
59 | // - webdriver, webdriverio
60 | // - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
61 | // - @wdio/mocha-framework, @wdio/jasmine-framework
62 | // - @wdio/local-runner, @wdio/lambda-runner
63 | // - @wdio/sumologic-reporter
64 | // - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
65 | // Level of logging verbosity: trace | debug | info | warn | error | silent
66 | // logLevels: {
67 | // webdriver: 'info',
68 | // '@wdio/applitools-service': 'info'
69 | // },
70 | //
71 | // If you only want to run your tests until a specific amount of tests have failed use
72 | // bail (default is 0 - don't bail, run all tests).
73 | bail: 0,
74 | //
75 | // Set a base URL in order to shorten url command calls. If your `url` parameter starts
76 | // with `/`, the base url gets prepended, not including the path portion of your baseUrl.
77 | // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
78 | // gets prepended directly.
79 | baseUrl: "http://localhost:3000",
80 | //
81 | // Default timeout for all waitFor* commands.
82 | waitforTimeout: 10000,
83 | //
84 | // Default timeout in milliseconds for request
85 | // if browser driver or grid doesn't send response
86 | connectionRetryTimeout: 90000,
87 | //
88 | // Default request retries count
89 | connectionRetryCount: 3,
90 | //
91 | // Framework you want to run your specs with.
92 | // The following are supported: Mocha, Jasmine, and Cucumber
93 | // see also: https://webdriver.io/docs/frameworks.html
94 | //
95 | // Make sure you have the wdio adapter package for the specific framework installed
96 | // before running any tests.
97 | framework: "jasmine",
98 | //
99 | // The number of times to retry the entire specfile when it fails as a whole
100 | specFileRetries: 1,
101 | //
102 | // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue
103 | specFileRetriesDeferred: true,
104 | //
105 | // Test reporter for stdout.
106 | // The only one supported by default is 'dot'
107 | // see also: https://webdriver.io/docs/dot-reporter.html
108 | reporters: ["spec"],
109 | //
110 | // Options to be passed to Jasmine.
111 | jasmineOpts: {
112 | // Jasmine default timeout
113 | defaultTimeoutInterval: 60000,
114 | //
115 | // The Jasmine framework allows interception of each assertion in order to log the state of the application
116 | // or website depending on the result. For example, it is pretty handy to take a screenshot every time
117 | // an assertion fails.
118 | // expectationResultHandler: function(passed, assertion) {
119 | // do something
120 | // }
121 | },
122 | //
123 | // =====
124 | // Hooks
125 | // =====
126 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
127 | // it and to build services around it. You can either apply a single function or an array of
128 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
129 | // resolved to continue.
130 | /**
131 | * Gets executed once before all workers get launched.
132 | * @param {Object} config wdio configuration object
133 | * @param {Array.} capabilities list of capabilities details
134 | */
135 | // onPrepare: function (config, capabilities) {
136 | // },
137 | /**
138 | * Gets executed before a worker process is spawned and can be used to initialise specific service
139 | * for that worker as well as modify runtime environments in an async fashion.
140 | * @param {String} cid capability id (e.g 0-0)
141 | * @param {[type]} caps object containing capabilities for session that will be spawn in the worker
142 | * @param {[type]} specs specs to be run in the worker process
143 | * @param {[type]} args object that will be merged with the main configuration once worker is initialised
144 | * @param {[type]} execArgv list of string arguments passed to the worker process
145 | */
146 | // onWorkerStart: function (cid, caps, specs, args, execArgv) {
147 | // },
148 | /**
149 | * Gets executed just before initialising the webdriver session and test framework. It allows you
150 | * to manipulate configurations depending on the capability or spec.
151 | * @param {Object} config wdio configuration object
152 | * @param {Array.} capabilities list of capabilities details
153 | * @param {Array.} specs List of spec file paths that are to be run
154 | */
155 | // beforeSession: function (config, capabilities, specs) {
156 | // },
157 | /**
158 | * Gets executed before test execution begins. At this point you can access to all global
159 | * variables like `browser`. It is the perfect place to define custom commands.
160 | * @param {Array.} capabilities list of capabilities details
161 | * @param {Array.} specs List of spec file paths that are to be run
162 | */
163 | // before: function (capabilities, specs) {
164 | // },
165 | /**
166 | * Runs before a WebdriverIO command gets executed.
167 | * @param {String} commandName hook command name
168 | * @param {Array} args arguments that command would receive
169 | */
170 | // beforeCommand: function (commandName, args) {
171 | // },
172 | /**
173 | * Hook that gets executed before the suite starts
174 | * @param {Object} suite suite details
175 | */
176 | // beforeSuite: function (suite) {
177 | // },
178 | /**
179 | * Function to be executed before a test (in Mocha/Jasmine) starts.
180 | */
181 | // beforeTest: function (test, context) {
182 | // },
183 | /**
184 | * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
185 | * beforeEach in Mocha)
186 | */
187 | // beforeHook: function (test, context) {
188 | // },
189 | /**
190 | * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
191 | * afterEach in Mocha)
192 | */
193 | // afterHook: function (test, context, { error, result, duration, passed, retries }) {
194 | // },
195 | /**
196 | * Function to be executed after a test (in Mocha/Jasmine).
197 | */
198 | // afterTest: function(test, context, { error, result, duration, passed, retries }) {
199 | // },
200 |
201 | /**
202 | * Hook that gets executed after the suite has ended
203 | * @param {Object} suite suite details
204 | */
205 | // afterSuite: function (suite) {
206 | // },
207 | /**
208 | * Runs after a WebdriverIO command gets executed
209 | * @param {String} commandName hook command name
210 | * @param {Array} args arguments that command would receive
211 | * @param {Number} result 0 - command success, 1 - command error
212 | * @param {Object} error error object if any
213 | */
214 | // afterCommand: function (commandName, args, result, error) {
215 | // },
216 | /**
217 | * Gets executed after all tests are done. You still have access to all global variables from
218 | * the test.
219 | * @param {Number} result 0 - test pass, 1 - test fail
220 | * @param {Array.} capabilities list of capabilities details
221 | * @param {Array.} specs List of spec file paths that ran
222 | */
223 | // after: function (result, capabilities, specs) {
224 | // },
225 | /**
226 | * Gets executed right after terminating the webdriver session.
227 | * @param {Object} config wdio configuration object
228 | * @param {Array.} capabilities list of capabilities details
229 | * @param {Array.} specs List of spec file paths that ran
230 | */
231 | // afterSession: function (config, capabilities, specs) {
232 | // },
233 | /**
234 | * Gets executed after all workers got shut down and the process is about to exit. An error
235 | * thrown in the onComplete hook will result in the test run failing.
236 | * @param {Object} exitCode 0 - success, 1 - fail
237 | * @param {Object} config wdio configuration object
238 | * @param {Array.} capabilities list of capabilities details
239 | * @param {} results object containing test results
240 | */
241 | // onComplete: function(exitCode, config, capabilities, results) {
242 | // },
243 | /**
244 | * Gets executed when a refresh happens.
245 | * @param {String} oldSessionId session ID of the old session
246 | * @param {String} newSessionId session ID of the new session
247 | */
248 | //onReload: function(oldSessionId, newSessionId) {
249 | //}
250 | };
251 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | testEnvironment: "jsdom",
4 | transform: {
5 | "^.+\\.(t|j)sx?$": ["@swc/jest"],
6 | },
7 | modulePathIgnorePatterns: ["e2e"],
8 | };
9 |
--------------------------------------------------------------------------------
/lib/api.ts:
--------------------------------------------------------------------------------
1 | import { util } from "./util";
2 | import logger from "./logger";
3 | import type { PeerJSOption } from "./optionInterfaces";
4 | import { version } from "../package.json";
5 |
6 | export class API {
7 | constructor(private readonly _options: PeerJSOption) {}
8 |
9 | private _buildRequest(method: string): Promise {
10 | const protocol = this._options.secure ? "https" : "http";
11 | const { host, port, path, key } = this._options;
12 | const url = new URL(`${protocol}://${host}:${port}${path}${key}/${method}`);
13 | // TODO: Why timestamp, why random?
14 | url.searchParams.set("ts", `${Date.now()}${Math.random()}`);
15 | url.searchParams.set("version", version);
16 | return fetch(url.href, {
17 | referrerPolicy: this._options.referrerPolicy,
18 | });
19 | }
20 |
21 | /** Get a unique ID from the server via XHR and initialize with it. */
22 | async retrieveId(): Promise {
23 | try {
24 | const response = await this._buildRequest("id");
25 |
26 | if (response.status !== 200) {
27 | throw new Error(`Error. Status:${response.status}`);
28 | }
29 |
30 | return response.text();
31 | } catch (error) {
32 | logger.error("Error retrieving ID", error);
33 |
34 | let pathError = "";
35 |
36 | if (
37 | this._options.path === "/" &&
38 | this._options.host !== util.CLOUD_HOST
39 | ) {
40 | pathError =
41 | " If you passed in a `path` to your self-hosted PeerServer, " +
42 | "you'll also need to pass in that same path when creating a new " +
43 | "Peer.";
44 | }
45 |
46 | throw new Error("Could not get an ID from the server." + pathError);
47 | }
48 | }
49 |
50 | /** @deprecated */
51 | async listAllPeers(): Promise {
52 | try {
53 | const response = await this._buildRequest("peers");
54 |
55 | if (response.status !== 200) {
56 | if (response.status === 401) {
57 | let helpfulError = "";
58 |
59 | if (this._options.host === util.CLOUD_HOST) {
60 | helpfulError =
61 | "It looks like you're using the cloud server. You can email " +
62 | "team@peerjs.com to enable peer listing for your API key.";
63 | } else {
64 | helpfulError =
65 | "You need to enable `allow_discovery` on your self-hosted " +
66 | "PeerServer to use this feature.";
67 | }
68 |
69 | throw new Error(
70 | "It doesn't look like you have permission to list peers IDs. " +
71 | helpfulError,
72 | );
73 | }
74 |
75 | throw new Error(`Error. Status:${response.status}`);
76 | }
77 |
78 | return response.json();
79 | } catch (error) {
80 | logger.error("Error retrieving list peers", error);
81 |
82 | throw new Error("Could not get list peers from the server." + error);
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/lib/baseconnection.ts:
--------------------------------------------------------------------------------
1 | import type { Peer } from "./peer";
2 | import type { ServerMessage } from "./servermessage";
3 | import type { ConnectionType } from "./enums";
4 | import { BaseConnectionErrorType } from "./enums";
5 | import {
6 | EventEmitterWithError,
7 | type EventsWithError,
8 | PeerError,
9 | } from "./peerError";
10 | import type { ValidEventTypes } from "eventemitter3";
11 |
12 | export interface BaseConnectionEvents<
13 | ErrorType extends string = BaseConnectionErrorType,
14 | > extends EventsWithError {
15 | /**
16 | * Emitted when either you or the remote peer closes the connection.
17 | *
18 | * ```ts
19 | * connection.on('close', () => { ... });
20 | * ```
21 | */
22 | close: () => void;
23 | /**
24 | * ```ts
25 | * connection.on('error', (error) => { ... });
26 | * ```
27 | */
28 | error: (error: PeerError<`${ErrorType}`>) => void;
29 | iceStateChanged: (state: RTCIceConnectionState) => void;
30 | }
31 |
32 | export abstract class BaseConnection<
33 | SubClassEvents extends ValidEventTypes,
34 | ErrorType extends string = never,
35 | > extends EventEmitterWithError<
36 | ErrorType | BaseConnectionErrorType,
37 | SubClassEvents & BaseConnectionEvents
38 | > {
39 | protected _open = false;
40 |
41 | /**
42 | * Any type of metadata associated with the connection,
43 | * passed in by whoever initiated the connection.
44 | */
45 | readonly metadata: any;
46 | connectionId: string;
47 |
48 | peerConnection: RTCPeerConnection;
49 | dataChannel: RTCDataChannel;
50 |
51 | abstract get type(): ConnectionType;
52 |
53 | /**
54 | * The optional label passed in or assigned by PeerJS when the connection was initiated.
55 | */
56 | label: string;
57 |
58 | /**
59 | * Whether the media connection is active (e.g. your call has been answered).
60 | * You can check this if you want to set a maximum wait time for a one-sided call.
61 | */
62 | get open() {
63 | return this._open;
64 | }
65 |
66 | protected constructor(
67 | /**
68 | * The ID of the peer on the other end of this connection.
69 | */
70 | readonly peer: string,
71 | public provider: Peer,
72 | readonly options: any,
73 | ) {
74 | super();
75 |
76 | this.metadata = options.metadata;
77 | }
78 |
79 | abstract close(): void;
80 |
81 | /**
82 | * @internal
83 | */
84 | abstract handleMessage(message: ServerMessage): void;
85 |
86 | /**
87 | * Called by the Negotiator when the DataChannel is ready.
88 | * @internal
89 | * */
90 | abstract _initializeDataChannel(dc: RTCDataChannel): void;
91 | }
92 |
--------------------------------------------------------------------------------
/lib/dataconnection/BufferedConnection/BinaryPack.ts:
--------------------------------------------------------------------------------
1 | import { BinaryPackChunker, concatArrayBuffers } from "./binaryPackChunker";
2 | import logger from "../../logger";
3 | import type { Peer } from "../../peer";
4 | import { BufferedConnection } from "./BufferedConnection";
5 | import { SerializationType } from "../../enums";
6 | import { pack, type Packable, unpack } from "peerjs-js-binarypack";
7 |
8 | export class BinaryPack extends BufferedConnection {
9 | private readonly chunker = new BinaryPackChunker();
10 | readonly serialization = SerializationType.Binary;
11 |
12 | private _chunkedData: {
13 | [id: number]: {
14 | data: Uint8Array[];
15 | count: number;
16 | total: number;
17 | };
18 | } = {};
19 |
20 | public override close(options?: { flush?: boolean }) {
21 | super.close(options);
22 | this._chunkedData = {};
23 | }
24 |
25 | constructor(peerId: string, provider: Peer, options: any) {
26 | super(peerId, provider, options);
27 | }
28 |
29 | // Handles a DataChannel message.
30 | protected override _handleDataMessage({ data }: { data: Uint8Array }): void {
31 | const deserializedData = unpack(data);
32 |
33 | // PeerJS specific message
34 | const peerData = deserializedData["__peerData"];
35 | if (peerData) {
36 | if (peerData.type === "close") {
37 | this.close();
38 | return;
39 | }
40 |
41 | // Chunked data -- piece things back together.
42 | // @ts-ignore
43 | this._handleChunk(deserializedData);
44 | return;
45 | }
46 |
47 | this.emit("data", deserializedData);
48 | }
49 |
50 | private _handleChunk(data: {
51 | __peerData: number;
52 | n: number;
53 | total: number;
54 | data: ArrayBuffer;
55 | }): void {
56 | const id = data.__peerData;
57 | const chunkInfo = this._chunkedData[id] || {
58 | data: [],
59 | count: 0,
60 | total: data.total,
61 | };
62 |
63 | chunkInfo.data[data.n] = new Uint8Array(data.data);
64 | chunkInfo.count++;
65 | this._chunkedData[id] = chunkInfo;
66 |
67 | if (chunkInfo.total === chunkInfo.count) {
68 | // Clean up before making the recursive call to `_handleDataMessage`.
69 | delete this._chunkedData[id];
70 |
71 | // We've received all the chunks--time to construct the complete data.
72 | // const data = new Blob(chunkInfo.data);
73 | const data = concatArrayBuffers(chunkInfo.data);
74 | this._handleDataMessage({ data });
75 | }
76 | }
77 |
78 | protected override _send(data: Packable, chunked: boolean) {
79 | const blob = pack(data);
80 | if (blob instanceof Promise) {
81 | return this._send_blob(blob);
82 | }
83 |
84 | if (!chunked && blob.byteLength > this.chunker.chunkedMTU) {
85 | this._sendChunks(blob);
86 | return;
87 | }
88 |
89 | this._bufferedSend(blob);
90 | }
91 | private async _send_blob(blobPromise: Promise) {
92 | const blob = await blobPromise;
93 | if (blob.byteLength > this.chunker.chunkedMTU) {
94 | this._sendChunks(blob);
95 | return;
96 | }
97 |
98 | this._bufferedSend(blob);
99 | }
100 |
101 | private _sendChunks(blob: ArrayBuffer) {
102 | const blobs = this.chunker.chunk(blob);
103 | logger.log(`DC#${this.connectionId} Try to send ${blobs.length} chunks...`);
104 |
105 | for (const blob of blobs) {
106 | this.send(blob, true);
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/lib/dataconnection/BufferedConnection/BufferedConnection.ts:
--------------------------------------------------------------------------------
1 | import logger from "../../logger";
2 | import { DataConnection } from "../DataConnection";
3 |
4 | export abstract class BufferedConnection extends DataConnection {
5 | private _buffer: any[] = [];
6 | private _bufferSize = 0;
7 | private _buffering = false;
8 |
9 | public get bufferSize(): number {
10 | return this._bufferSize;
11 | }
12 |
13 | public override _initializeDataChannel(dc: RTCDataChannel) {
14 | super._initializeDataChannel(dc);
15 | this.dataChannel.binaryType = "arraybuffer";
16 | this.dataChannel.addEventListener("message", (e) =>
17 | this._handleDataMessage(e),
18 | );
19 | }
20 |
21 | protected abstract _handleDataMessage(e: MessageEvent): void;
22 |
23 | protected _bufferedSend(msg: ArrayBuffer): void {
24 | if (this._buffering || !this._trySend(msg)) {
25 | this._buffer.push(msg);
26 | this._bufferSize = this._buffer.length;
27 | }
28 | }
29 |
30 | // Returns true if the send succeeds.
31 | private _trySend(msg: ArrayBuffer): boolean {
32 | if (!this.open) {
33 | return false;
34 | }
35 |
36 | if (this.dataChannel.bufferedAmount > DataConnection.MAX_BUFFERED_AMOUNT) {
37 | this._buffering = true;
38 | setTimeout(() => {
39 | this._buffering = false;
40 | this._tryBuffer();
41 | }, 50);
42 |
43 | return false;
44 | }
45 |
46 | try {
47 | this.dataChannel.send(msg);
48 | } catch (e) {
49 | logger.error(`DC#:${this.connectionId} Error when sending:`, e);
50 | this._buffering = true;
51 |
52 | this.close();
53 |
54 | return false;
55 | }
56 |
57 | return true;
58 | }
59 |
60 | // Try to send the first message in the buffer.
61 | private _tryBuffer(): void {
62 | if (!this.open) {
63 | return;
64 | }
65 |
66 | if (this._buffer.length === 0) {
67 | return;
68 | }
69 |
70 | const msg = this._buffer[0];
71 |
72 | if (this._trySend(msg)) {
73 | this._buffer.shift();
74 | this._bufferSize = this._buffer.length;
75 | this._tryBuffer();
76 | }
77 | }
78 |
79 | public override close(options?: { flush?: boolean }) {
80 | if (options?.flush) {
81 | this.send({
82 | __peerData: {
83 | type: "close",
84 | },
85 | });
86 | return;
87 | }
88 | this._buffer = [];
89 | this._bufferSize = 0;
90 | super.close();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/dataconnection/BufferedConnection/Json.ts:
--------------------------------------------------------------------------------
1 | import { BufferedConnection } from "./BufferedConnection";
2 | import { DataConnectionErrorType, SerializationType } from "../../enums";
3 | import { util } from "../../util";
4 |
5 | export class Json extends BufferedConnection {
6 | readonly serialization = SerializationType.JSON;
7 | private readonly encoder = new TextEncoder();
8 | private readonly decoder = new TextDecoder();
9 |
10 | stringify: (data: any) => string = JSON.stringify;
11 | parse: (data: string) => any = JSON.parse;
12 |
13 | // Handles a DataChannel message.
14 | protected override _handleDataMessage({ data }: { data: Uint8Array }): void {
15 | const deserializedData = this.parse(this.decoder.decode(data));
16 |
17 | // PeerJS specific message
18 | const peerData = deserializedData["__peerData"];
19 | if (peerData && peerData.type === "close") {
20 | this.close();
21 | return;
22 | }
23 |
24 | this.emit("data", deserializedData);
25 | }
26 |
27 | override _send(data, _chunked) {
28 | const encodedData = this.encoder.encode(this.stringify(data));
29 | if (encodedData.byteLength >= util.chunkedMTU) {
30 | this.emitError(
31 | DataConnectionErrorType.MessageToBig,
32 | "Message too big for JSON channel",
33 | );
34 | return;
35 | }
36 | this._bufferedSend(encodedData);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/dataconnection/BufferedConnection/Raw.ts:
--------------------------------------------------------------------------------
1 | import { BufferedConnection } from "./BufferedConnection";
2 | import { SerializationType } from "../../enums";
3 |
4 | export class Raw extends BufferedConnection {
5 | readonly serialization = SerializationType.None;
6 |
7 | protected _handleDataMessage({ data }) {
8 | super.emit("data", data);
9 | }
10 |
11 | override _send(data, _chunked) {
12 | this._bufferedSend(data);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/dataconnection/BufferedConnection/binaryPackChunker.ts:
--------------------------------------------------------------------------------
1 | export class BinaryPackChunker {
2 | readonly chunkedMTU = 16300; // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
3 |
4 | // Binary stuff
5 |
6 | private _dataCount: number = 1;
7 |
8 | chunk = (
9 | blob: ArrayBuffer,
10 | ): { __peerData: number; n: number; total: number; data: Uint8Array }[] => {
11 | const chunks = [];
12 | const size = blob.byteLength;
13 | const total = Math.ceil(size / this.chunkedMTU);
14 |
15 | let index = 0;
16 | let start = 0;
17 |
18 | while (start < size) {
19 | const end = Math.min(size, start + this.chunkedMTU);
20 | const b = blob.slice(start, end);
21 |
22 | const chunk = {
23 | __peerData: this._dataCount,
24 | n: index,
25 | data: b,
26 | total,
27 | };
28 |
29 | chunks.push(chunk);
30 |
31 | start = end;
32 | index++;
33 | }
34 |
35 | this._dataCount++;
36 |
37 | return chunks;
38 | };
39 | }
40 |
41 | export function concatArrayBuffers(bufs: Uint8Array[]) {
42 | let size = 0;
43 | for (const buf of bufs) {
44 | size += buf.byteLength;
45 | }
46 | const result = new Uint8Array(size);
47 | let offset = 0;
48 | for (const buf of bufs) {
49 | result.set(buf, offset);
50 | offset += buf.byteLength;
51 | }
52 | return result;
53 | }
54 |
--------------------------------------------------------------------------------
/lib/dataconnection/DataConnection.ts:
--------------------------------------------------------------------------------
1 | import logger from "../logger";
2 | import { Negotiator } from "../negotiator";
3 | import {
4 | BaseConnectionErrorType,
5 | ConnectionType,
6 | DataConnectionErrorType,
7 | ServerMessageType,
8 | } from "../enums";
9 | import type { Peer } from "../peer";
10 | import { BaseConnection, type BaseConnectionEvents } from "../baseconnection";
11 | import type { ServerMessage } from "../servermessage";
12 | import type { EventsWithError } from "../peerError";
13 | import { randomToken } from "../utils/randomToken";
14 |
15 | export interface DataConnectionEvents
16 | extends EventsWithError,
17 | BaseConnectionEvents {
18 | /**
19 | * Emitted when data is received from the remote peer.
20 | */
21 | data: (data: unknown) => void;
22 | /**
23 | * Emitted when the connection is established and ready-to-use.
24 | */
25 | open: () => void;
26 | }
27 |
28 | /**
29 | * Wraps a DataChannel between two Peers.
30 | */
31 | export abstract class DataConnection extends BaseConnection<
32 | DataConnectionEvents,
33 | DataConnectionErrorType
34 | > {
35 | protected static readonly ID_PREFIX = "dc_";
36 | protected static readonly MAX_BUFFERED_AMOUNT = 8 * 1024 * 1024;
37 |
38 | private _negotiator: Negotiator;
39 | abstract readonly serialization: string;
40 | readonly reliable: boolean;
41 |
42 | public get type() {
43 | return ConnectionType.Data;
44 | }
45 |
46 | constructor(peerId: string, provider: Peer, options: any) {
47 | super(peerId, provider, options);
48 |
49 | this.connectionId =
50 | this.options.connectionId || DataConnection.ID_PREFIX + randomToken();
51 |
52 | this.label = this.options.label || this.connectionId;
53 | this.reliable = !!this.options.reliable;
54 |
55 | this._negotiator = new Negotiator(this);
56 |
57 | this._negotiator.startConnection(
58 | this.options._payload || {
59 | originator: true,
60 | reliable: this.reliable,
61 | },
62 | );
63 | }
64 |
65 | /** Called by the Negotiator when the DataChannel is ready. */
66 | override _initializeDataChannel(dc: RTCDataChannel): void {
67 | this.dataChannel = dc;
68 |
69 | this.dataChannel.onopen = () => {
70 | logger.log(`DC#${this.connectionId} dc connection success`);
71 | this._open = true;
72 | this.emit("open");
73 | };
74 |
75 | this.dataChannel.onmessage = (e) => {
76 | logger.log(`DC#${this.connectionId} dc onmessage:`, e.data);
77 | // this._handleDataMessage(e);
78 | };
79 |
80 | this.dataChannel.onclose = () => {
81 | logger.log(`DC#${this.connectionId} dc closed for:`, this.peer);
82 | this.close();
83 | };
84 | }
85 |
86 | /**
87 | * Exposed functionality for users.
88 | */
89 |
90 | /** Allows user to close connection. */
91 | close(options?: { flush?: boolean }): void {
92 | if (options?.flush) {
93 | this.send({
94 | __peerData: {
95 | type: "close",
96 | },
97 | });
98 | return;
99 | }
100 | if (this._negotiator) {
101 | this._negotiator.cleanup();
102 | this._negotiator = null;
103 | }
104 |
105 | if (this.provider) {
106 | this.provider._removeConnection(this);
107 |
108 | this.provider = null;
109 | }
110 |
111 | if (this.dataChannel) {
112 | this.dataChannel.onopen = null;
113 | this.dataChannel.onmessage = null;
114 | this.dataChannel.onclose = null;
115 | this.dataChannel = null;
116 | }
117 |
118 | if (!this.open) {
119 | return;
120 | }
121 |
122 | this._open = false;
123 |
124 | super.emit("close");
125 | }
126 |
127 | protected abstract _send(data: any, chunked: boolean): void | Promise;
128 |
129 | /** Allows user to send data. */
130 | public send(data: any, chunked = false) {
131 | if (!this.open) {
132 | this.emitError(
133 | DataConnectionErrorType.NotOpenYet,
134 | "Connection is not open. You should listen for the `open` event before sending messages.",
135 | );
136 | return;
137 | }
138 | return this._send(data, chunked);
139 | }
140 |
141 | async handleMessage(message: ServerMessage) {
142 | const payload = message.payload;
143 |
144 | switch (message.type) {
145 | case ServerMessageType.Answer:
146 | await this._negotiator.handleSDP(message.type, payload.sdp);
147 | break;
148 | case ServerMessageType.Candidate:
149 | await this._negotiator.handleCandidate(payload.candidate);
150 | break;
151 | default:
152 | logger.warn(
153 | "Unrecognized message type:",
154 | message.type,
155 | "from peer:",
156 | this.peer,
157 | );
158 | break;
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/lib/dataconnection/StreamConnection/MsgPack.ts:
--------------------------------------------------------------------------------
1 | import { decodeMultiStream, Encoder } from "@msgpack/msgpack";
2 | import { StreamConnection } from "./StreamConnection.js";
3 | import type { Peer } from "../../peer.js";
4 |
5 | export class MsgPack extends StreamConnection {
6 | readonly serialization = "MsgPack";
7 | private _encoder = new Encoder();
8 |
9 | constructor(peerId: string, provider: Peer, options: any) {
10 | super(peerId, provider, options);
11 |
12 | (async () => {
13 | for await (const msg of decodeMultiStream(this._rawReadStream)) {
14 | // @ts-ignore
15 | if (msg.__peerData?.type === "close") {
16 | this.close();
17 | return;
18 | }
19 | this.emit("data", msg);
20 | }
21 | })();
22 | }
23 |
24 | protected override _send(data) {
25 | return this.writer.write(this._encoder.encode(data));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/dataconnection/StreamConnection/StreamConnection.ts:
--------------------------------------------------------------------------------
1 | import logger from "../../logger.js";
2 | import type { Peer } from "../../peer.js";
3 | import { DataConnection } from "../DataConnection.js";
4 |
5 | export abstract class StreamConnection extends DataConnection {
6 | private _CHUNK_SIZE = 1024 * 8 * 4;
7 | private _splitStream = new TransformStream({
8 | transform: (chunk, controller) => {
9 | for (let split = 0; split < chunk.length; split += this._CHUNK_SIZE) {
10 | controller.enqueue(chunk.subarray(split, split + this._CHUNK_SIZE));
11 | }
12 | },
13 | });
14 | private _rawSendStream = new WritableStream({
15 | write: async (chunk, controller) => {
16 | const openEvent = new Promise((resolve) =>
17 | this.dataChannel.addEventListener("bufferedamountlow", resolve, {
18 | once: true,
19 | }),
20 | );
21 |
22 | // if we can send the chunk now, send it
23 | // if not, we wait until at least half of the sending buffer is free again
24 | await (this.dataChannel.bufferedAmount <=
25 | DataConnection.MAX_BUFFERED_AMOUNT - chunk.byteLength || openEvent);
26 |
27 | // TODO: what can go wrong here?
28 | try {
29 | this.dataChannel.send(chunk);
30 | } catch (e) {
31 | logger.error(`DC#:${this.connectionId} Error when sending:`, e);
32 | controller.error(e);
33 | this.close();
34 | }
35 | },
36 | });
37 | protected writer = this._splitStream.writable.getWriter();
38 |
39 | protected _rawReadStream = new ReadableStream({
40 | start: (controller) => {
41 | this.once("open", () => {
42 | this.dataChannel.addEventListener("message", (e) => {
43 | controller.enqueue(e.data);
44 | });
45 | });
46 | },
47 | });
48 |
49 | protected constructor(peerId: string, provider: Peer, options: any) {
50 | super(peerId, provider, { ...options, reliable: true });
51 |
52 | void this._splitStream.readable.pipeTo(this._rawSendStream);
53 | }
54 |
55 | public override _initializeDataChannel(dc) {
56 | super._initializeDataChannel(dc);
57 | this.dataChannel.binaryType = "arraybuffer";
58 | this.dataChannel.bufferedAmountLowThreshold =
59 | DataConnection.MAX_BUFFERED_AMOUNT / 2;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/encodingQueue.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "eventemitter3";
2 | import logger from "./logger";
3 |
4 | export class EncodingQueue extends EventEmitter {
5 | readonly fileReader: FileReader = new FileReader();
6 |
7 | private _queue: Blob[] = [];
8 | private _processing: boolean = false;
9 |
10 | constructor() {
11 | super();
12 |
13 | this.fileReader.onload = (evt) => {
14 | this._processing = false;
15 |
16 | if (evt.target) {
17 | this.emit("done", evt.target.result as ArrayBuffer);
18 | }
19 |
20 | this.doNextTask();
21 | };
22 |
23 | this.fileReader.onerror = (evt) => {
24 | logger.error(`EncodingQueue error:`, evt);
25 | this._processing = false;
26 | this.destroy();
27 | this.emit("error", evt);
28 | };
29 | }
30 |
31 | get queue(): Blob[] {
32 | return this._queue;
33 | }
34 |
35 | get size(): number {
36 | return this.queue.length;
37 | }
38 |
39 | get processing(): boolean {
40 | return this._processing;
41 | }
42 |
43 | enque(blob: Blob): void {
44 | this.queue.push(blob);
45 |
46 | if (this.processing) return;
47 |
48 | this.doNextTask();
49 | }
50 |
51 | destroy(): void {
52 | this.fileReader.abort();
53 | this._queue = [];
54 | }
55 |
56 | private doNextTask(): void {
57 | if (this.size === 0) return;
58 | if (this.processing) return;
59 |
60 | this._processing = true;
61 |
62 | this.fileReader.readAsArrayBuffer(this.queue.shift());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/enums.ts:
--------------------------------------------------------------------------------
1 | export enum ConnectionType {
2 | Data = "data",
3 | Media = "media",
4 | }
5 |
6 | export enum PeerErrorType {
7 | /**
8 | * The client's browser does not support some or all WebRTC features that you are trying to use.
9 | */
10 | BrowserIncompatible = "browser-incompatible",
11 | /**
12 | * You've already disconnected this peer from the server and can no longer make any new connections on it.
13 | */
14 | Disconnected = "disconnected",
15 | /**
16 | * The ID passed into the Peer constructor contains illegal characters.
17 | */
18 | InvalidID = "invalid-id",
19 | /**
20 | * The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only).
21 | */
22 | InvalidKey = "invalid-key",
23 | /**
24 | * Lost or cannot establish a connection to the signalling server.
25 | */
26 | Network = "network",
27 | /**
28 | * The peer you're trying to connect to does not exist.
29 | */
30 | PeerUnavailable = "peer-unavailable",
31 | /**
32 | * PeerJS is being used securely, but the cloud server does not support SSL. Use a custom PeerServer.
33 | */
34 | SslUnavailable = "ssl-unavailable",
35 | /**
36 | * Unable to reach the server.
37 | */
38 | ServerError = "server-error",
39 | /**
40 | * An error from the underlying socket.
41 | */
42 | SocketError = "socket-error",
43 | /**
44 | * The underlying socket closed unexpectedly.
45 | */
46 | SocketClosed = "socket-closed",
47 | /**
48 | * The ID passed into the Peer constructor is already taken.
49 | *
50 | * :::caution
51 | * This error is not fatal if your peer has open peer-to-peer connections.
52 | * This can happen if you attempt to {@apilink Peer.reconnect} a peer that has been disconnected from the server,
53 | * but its old ID has now been taken.
54 | * :::
55 | */
56 | UnavailableID = "unavailable-id",
57 | /**
58 | * Native WebRTC errors.
59 | */
60 | WebRTC = "webrtc",
61 | }
62 |
63 | export enum BaseConnectionErrorType {
64 | NegotiationFailed = "negotiation-failed",
65 | ConnectionClosed = "connection-closed",
66 | }
67 |
68 | export enum DataConnectionErrorType {
69 | NotOpenYet = "not-open-yet",
70 | MessageToBig = "message-too-big",
71 | }
72 |
73 | export enum SerializationType {
74 | Binary = "binary",
75 | BinaryUTF8 = "binary-utf8",
76 | JSON = "json",
77 | None = "raw",
78 | }
79 |
80 | export enum SocketEventType {
81 | Message = "message",
82 | Disconnected = "disconnected",
83 | Error = "error",
84 | Close = "close",
85 | }
86 |
87 | export enum ServerMessageType {
88 | Heartbeat = "HEARTBEAT",
89 | Candidate = "CANDIDATE",
90 | Offer = "OFFER",
91 | Answer = "ANSWER",
92 | Open = "OPEN", // The connection to the server is open.
93 | Error = "ERROR", // Server error.
94 | IdTaken = "ID-TAKEN", // The selected ID is taken.
95 | InvalidKey = "INVALID-KEY", // The given API key cannot be found.
96 | Leave = "LEAVE", // Another peer has closed its connection to this peer.
97 | Expire = "EXPIRE", // The offer sent to a peer has expired without response.
98 | }
99 |
--------------------------------------------------------------------------------
/lib/exports.ts:
--------------------------------------------------------------------------------
1 | export { util, type Util } from "./util";
2 | import { Peer } from "./peer";
3 | import { MsgPackPeer } from "./msgPackPeer";
4 |
5 | export type { PeerEvents, PeerOptions } from "./peer";
6 |
7 | export type {
8 | PeerJSOption,
9 | PeerConnectOption,
10 | AnswerOption,
11 | CallOption,
12 | } from "./optionInterfaces";
13 | export type { UtilSupportsObj } from "./util";
14 | export type { DataConnection } from "./dataconnection/DataConnection";
15 | export type { MediaConnection } from "./mediaconnection";
16 | export type { LogLevel } from "./logger";
17 | export * from "./enums";
18 |
19 | export { BufferedConnection } from "./dataconnection/BufferedConnection/BufferedConnection";
20 | export { StreamConnection } from "./dataconnection/StreamConnection/StreamConnection";
21 | export { MsgPack } from "./dataconnection/StreamConnection/MsgPack";
22 | export type { SerializerMapping } from "./peer";
23 |
24 | export { Peer, MsgPackPeer };
25 |
26 | export { PeerError } from "./peerError";
27 | export default Peer;
28 |
--------------------------------------------------------------------------------
/lib/global.ts:
--------------------------------------------------------------------------------
1 | import { util } from "./util";
2 | import { Peer } from "./peer";
3 |
4 | (window).peerjs = {
5 | Peer,
6 | util,
7 | };
8 | /** @deprecated Should use peerjs namespace */
9 | (window).Peer = Peer;
10 |
--------------------------------------------------------------------------------
/lib/logger.ts:
--------------------------------------------------------------------------------
1 | const LOG_PREFIX = "PeerJS: ";
2 |
3 | /*
4 | Prints log messages depending on the debug level passed in. Defaults to 0.
5 | 0 Prints no logs.
6 | 1 Prints only errors.
7 | 2 Prints errors and warnings.
8 | 3 Prints all logs.
9 | */
10 | export enum LogLevel {
11 | /**
12 | * Prints no logs.
13 | */
14 | Disabled,
15 | /**
16 | * Prints only errors.
17 | */
18 | Errors,
19 | /**
20 | * Prints errors and warnings.
21 | */
22 | Warnings,
23 | /**
24 | * Prints all logs.
25 | */
26 | All,
27 | }
28 |
29 | class Logger {
30 | private _logLevel = LogLevel.Disabled;
31 |
32 | get logLevel(): LogLevel {
33 | return this._logLevel;
34 | }
35 |
36 | set logLevel(logLevel: LogLevel) {
37 | this._logLevel = logLevel;
38 | }
39 |
40 | log(...args: any[]) {
41 | if (this._logLevel >= LogLevel.All) {
42 | this._print(LogLevel.All, ...args);
43 | }
44 | }
45 |
46 | warn(...args: any[]) {
47 | if (this._logLevel >= LogLevel.Warnings) {
48 | this._print(LogLevel.Warnings, ...args);
49 | }
50 | }
51 |
52 | error(...args: any[]) {
53 | if (this._logLevel >= LogLevel.Errors) {
54 | this._print(LogLevel.Errors, ...args);
55 | }
56 | }
57 |
58 | setLogFunction(fn: (logLevel: LogLevel, ..._: any[]) => void): void {
59 | this._print = fn;
60 | }
61 |
62 | private _print(logLevel: LogLevel, ...rest: any[]): void {
63 | const copy = [LOG_PREFIX, ...rest];
64 |
65 | for (const i in copy) {
66 | if (copy[i] instanceof Error) {
67 | copy[i] = "(" + copy[i].name + ") " + copy[i].message;
68 | }
69 | }
70 |
71 | if (logLevel >= LogLevel.All) {
72 | console.log(...copy);
73 | } else if (logLevel >= LogLevel.Warnings) {
74 | console.warn("WARNING", ...copy);
75 | } else if (logLevel >= LogLevel.Errors) {
76 | console.error("ERROR", ...copy);
77 | }
78 | }
79 | }
80 |
81 | export default new Logger();
82 |
--------------------------------------------------------------------------------
/lib/mediaconnection.ts:
--------------------------------------------------------------------------------
1 | import { util } from "./util";
2 | import logger from "./logger";
3 | import { Negotiator } from "./negotiator";
4 | import { ConnectionType, ServerMessageType } from "./enums";
5 | import type { Peer } from "./peer";
6 | import { BaseConnection, type BaseConnectionEvents } from "./baseconnection";
7 | import type { ServerMessage } from "./servermessage";
8 | import type { AnswerOption } from "./optionInterfaces";
9 |
10 | export interface MediaConnectionEvents extends BaseConnectionEvents {
11 | /**
12 | * Emitted when a connection to the PeerServer is established.
13 | *
14 | * ```ts
15 | * mediaConnection.on('stream', (stream) => { ... });
16 | * ```
17 | */
18 | stream: (stream: MediaStream) => void;
19 | /**
20 | * Emitted when the auxiliary data channel is established.
21 | * After this event, hanging up will close the connection cleanly on the remote peer.
22 | * @beta
23 | */
24 | willCloseOnRemote: () => void;
25 | }
26 |
27 | /**
28 | * Wraps WebRTC's media streams.
29 | * To get one, use {@apilink Peer.call} or listen for the {@apilink PeerEvents | `call`} event.
30 | */
31 | export class MediaConnection extends BaseConnection {
32 | private static readonly ID_PREFIX = "mc_";
33 | readonly label: string;
34 |
35 | private _negotiator: Negotiator;
36 | private _localStream: MediaStream;
37 | private _remoteStream: MediaStream;
38 |
39 | /**
40 | * For media connections, this is always 'media'.
41 | */
42 | get type() {
43 | return ConnectionType.Media;
44 | }
45 |
46 | get localStream(): MediaStream {
47 | return this._localStream;
48 | }
49 |
50 | get remoteStream(): MediaStream {
51 | return this._remoteStream;
52 | }
53 |
54 | constructor(peerId: string, provider: Peer, options: any) {
55 | super(peerId, provider, options);
56 |
57 | this._localStream = this.options._stream;
58 | this.connectionId =
59 | this.options.connectionId ||
60 | MediaConnection.ID_PREFIX + util.randomToken();
61 |
62 | this._negotiator = new Negotiator(this);
63 |
64 | if (this._localStream) {
65 | this._negotiator.startConnection({
66 | _stream: this._localStream,
67 | originator: true,
68 | });
69 | }
70 | }
71 |
72 | /** Called by the Negotiator when the DataChannel is ready. */
73 | override _initializeDataChannel(dc: RTCDataChannel): void {
74 | this.dataChannel = dc;
75 |
76 | this.dataChannel.onopen = () => {
77 | logger.log(`DC#${this.connectionId} dc connection success`);
78 | this.emit("willCloseOnRemote");
79 | };
80 |
81 | this.dataChannel.onclose = () => {
82 | logger.log(`DC#${this.connectionId} dc closed for:`, this.peer);
83 | this.close();
84 | };
85 | }
86 | addStream(remoteStream) {
87 | logger.log("Receiving stream", remoteStream);
88 |
89 | this._remoteStream = remoteStream;
90 | super.emit("stream", remoteStream); // Should we call this `open`?
91 | }
92 |
93 | /**
94 | * @internal
95 | */
96 | handleMessage(message: ServerMessage): void {
97 | const type = message.type;
98 | const payload = message.payload;
99 |
100 | switch (message.type) {
101 | case ServerMessageType.Answer:
102 | // Forward to negotiator
103 | void this._negotiator.handleSDP(type, payload.sdp);
104 | this._open = true;
105 | break;
106 | case ServerMessageType.Candidate:
107 | void this._negotiator.handleCandidate(payload.candidate);
108 | break;
109 | default:
110 | logger.warn(`Unrecognized message type:${type} from peer:${this.peer}`);
111 | break;
112 | }
113 | }
114 |
115 | /**
116 | * When receiving a {@apilink PeerEvents | `call`} event on a peer, you can call
117 | * `answer` on the media connection provided by the callback to accept the call
118 | * and optionally send your own media stream.
119 |
120 | *
121 | * @param stream A WebRTC media stream.
122 | * @param options
123 | * @returns
124 | */
125 | answer(stream?: MediaStream, options: AnswerOption = {}): void {
126 | if (this._localStream) {
127 | logger.warn(
128 | "Local stream already exists on this MediaConnection. Are you answering a call twice?",
129 | );
130 | return;
131 | }
132 |
133 | this._localStream = stream;
134 |
135 | if (options && options.sdpTransform) {
136 | this.options.sdpTransform = options.sdpTransform;
137 | }
138 |
139 | this._negotiator.startConnection({
140 | ...this.options._payload,
141 | _stream: stream,
142 | });
143 | // Retrieve lost messages stored because PeerConnection not set up.
144 | const messages = this.provider._getMessages(this.connectionId);
145 |
146 | for (const message of messages) {
147 | this.handleMessage(message);
148 | }
149 |
150 | this._open = true;
151 | }
152 |
153 | /**
154 | * Exposed functionality for users.
155 | */
156 |
157 | /**
158 | * Closes the media connection.
159 | */
160 | close(): void {
161 | if (this._negotiator) {
162 | this._negotiator.cleanup();
163 | this._negotiator = null;
164 | }
165 |
166 | this._localStream = null;
167 | this._remoteStream = null;
168 |
169 | if (this.provider) {
170 | this.provider._removeConnection(this);
171 |
172 | this.provider = null;
173 | }
174 |
175 | if (this.options && this.options._stream) {
176 | this.options._stream = null;
177 | }
178 |
179 | if (!this.open) {
180 | return;
181 | }
182 |
183 | this._open = false;
184 |
185 | super.emit("close");
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/lib/msgPackPeer.ts:
--------------------------------------------------------------------------------
1 | import { Peer, type SerializerMapping } from "./peer";
2 | import { MsgPack } from "./exports";
3 |
4 | /**
5 | * @experimental
6 | */
7 | export class MsgPackPeer extends Peer {
8 | override _serializers: SerializerMapping = {
9 | MsgPack,
10 | default: MsgPack,
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/lib/negotiator.ts:
--------------------------------------------------------------------------------
1 | import logger from "./logger";
2 | import type { MediaConnection } from "./mediaconnection";
3 | import type { DataConnection } from "./dataconnection/DataConnection";
4 | import {
5 | BaseConnectionErrorType,
6 | ConnectionType,
7 | PeerErrorType,
8 | ServerMessageType,
9 | } from "./enums";
10 | import type { BaseConnection, BaseConnectionEvents } from "./baseconnection";
11 | import type { ValidEventTypes } from "eventemitter3";
12 |
13 | /**
14 | * Manages all negotiations between Peers.
15 | */
16 | export class Negotiator<
17 | Events extends ValidEventTypes,
18 | ConnectionType extends BaseConnection,
19 | > {
20 | constructor(readonly connection: ConnectionType) {}
21 |
22 | /** Returns a PeerConnection object set up correctly (for data, media). */
23 | startConnection(options: any) {
24 | const peerConnection = this._startPeerConnection();
25 |
26 | // Set the connection's PC.
27 | this.connection.peerConnection = peerConnection;
28 |
29 | if (this.connection.type === ConnectionType.Media && options._stream) {
30 | this._addTracksToConnection(options._stream, peerConnection);
31 | }
32 |
33 | // What do we need to do now?
34 | if (options.originator) {
35 | const dataConnection = this.connection;
36 |
37 | const config: RTCDataChannelInit = { ordered: !!options.reliable };
38 |
39 | const dataChannel = peerConnection.createDataChannel(
40 | dataConnection.label,
41 | config,
42 | );
43 | dataConnection._initializeDataChannel(dataChannel);
44 |
45 | void this._makeOffer();
46 | } else {
47 | void this.handleSDP("OFFER", options.sdp);
48 | }
49 | }
50 |
51 | /** Start a PC. */
52 | private _startPeerConnection(): RTCPeerConnection {
53 | logger.log("Creating RTCPeerConnection.");
54 |
55 | const peerConnection = new RTCPeerConnection(
56 | this.connection.provider.options.config,
57 | );
58 |
59 | this._setupListeners(peerConnection);
60 |
61 | return peerConnection;
62 | }
63 |
64 | /** Set up various WebRTC listeners. */
65 | private _setupListeners(peerConnection: RTCPeerConnection) {
66 | const peerId = this.connection.peer;
67 | const connectionId = this.connection.connectionId;
68 | const connectionType = this.connection.type;
69 | const provider = this.connection.provider;
70 |
71 | // ICE CANDIDATES.
72 | logger.log("Listening for ICE candidates.");
73 |
74 | peerConnection.onicecandidate = (evt) => {
75 | if (!evt.candidate || !evt.candidate.candidate) return;
76 |
77 | logger.log(`Received ICE candidates for ${peerId}:`, evt.candidate);
78 |
79 | provider.socket.send({
80 | type: ServerMessageType.Candidate,
81 | payload: {
82 | candidate: evt.candidate,
83 | type: connectionType,
84 | connectionId: connectionId,
85 | },
86 | dst: peerId,
87 | });
88 | };
89 |
90 | peerConnection.oniceconnectionstatechange = () => {
91 | switch (peerConnection.iceConnectionState) {
92 | case "failed":
93 | logger.log(
94 | "iceConnectionState is failed, closing connections to " + peerId,
95 | );
96 | this.connection.emitError(
97 | BaseConnectionErrorType.NegotiationFailed,
98 | "Negotiation of connection to " + peerId + " failed.",
99 | );
100 | this.connection.close();
101 | break;
102 | case "closed":
103 | logger.log(
104 | "iceConnectionState is closed, closing connections to " + peerId,
105 | );
106 | this.connection.emitError(
107 | BaseConnectionErrorType.ConnectionClosed,
108 | "Connection to " + peerId + " closed.",
109 | );
110 | this.connection.close();
111 | break;
112 | case "disconnected":
113 | logger.log(
114 | "iceConnectionState changed to disconnected on the connection with " +
115 | peerId,
116 | );
117 | break;
118 | case "completed":
119 | peerConnection.onicecandidate = () => {};
120 | break;
121 | }
122 |
123 | this.connection.emit(
124 | "iceStateChanged",
125 | peerConnection.iceConnectionState,
126 | );
127 | };
128 |
129 | // DATACONNECTION.
130 | logger.log("Listening for data channel");
131 | // Fired between offer and answer, so options should already be saved
132 | // in the options hash.
133 | peerConnection.ondatachannel = (evt) => {
134 | logger.log("Received data channel");
135 |
136 | const dataChannel = evt.channel;
137 | const connection = (
138 | provider.getConnection(peerId, connectionId)
139 | );
140 |
141 | connection._initializeDataChannel(dataChannel);
142 | };
143 |
144 | // MEDIACONNECTION.
145 | logger.log("Listening for remote stream");
146 |
147 | peerConnection.ontrack = (evt) => {
148 | logger.log("Received remote stream");
149 |
150 | const stream = evt.streams[0];
151 | const connection = provider.getConnection(peerId, connectionId);
152 |
153 | if (connection.type === ConnectionType.Media) {
154 | const mediaConnection = connection;
155 |
156 | this._addStreamToMediaConnection(stream, mediaConnection);
157 | }
158 | };
159 | }
160 |
161 | cleanup(): void {
162 | logger.log("Cleaning up PeerConnection to " + this.connection.peer);
163 |
164 | const peerConnection = this.connection.peerConnection;
165 |
166 | if (!peerConnection) {
167 | return;
168 | }
169 |
170 | this.connection.peerConnection = null;
171 |
172 | //unsubscribe from all PeerConnection's events
173 | peerConnection.onicecandidate =
174 | peerConnection.oniceconnectionstatechange =
175 | peerConnection.ondatachannel =
176 | peerConnection.ontrack =
177 | () => {};
178 |
179 | const peerConnectionNotClosed = peerConnection.signalingState !== "closed";
180 | let dataChannelNotClosed = false;
181 |
182 | const dataChannel = this.connection.dataChannel;
183 |
184 | if (dataChannel) {
185 | dataChannelNotClosed =
186 | !!dataChannel.readyState && dataChannel.readyState !== "closed";
187 | }
188 |
189 | if (peerConnectionNotClosed || dataChannelNotClosed) {
190 | peerConnection.close();
191 | }
192 | }
193 |
194 | private async _makeOffer(): Promise {
195 | const peerConnection = this.connection.peerConnection;
196 | const provider = this.connection.provider;
197 |
198 | try {
199 | const offer = await peerConnection.createOffer(
200 | this.connection.options.constraints,
201 | );
202 |
203 | logger.log("Created offer.");
204 |
205 | if (
206 | this.connection.options.sdpTransform &&
207 | typeof this.connection.options.sdpTransform === "function"
208 | ) {
209 | offer.sdp =
210 | this.connection.options.sdpTransform(offer.sdp) || offer.sdp;
211 | }
212 |
213 | try {
214 | await peerConnection.setLocalDescription(offer);
215 |
216 | logger.log(
217 | "Set localDescription:",
218 | offer,
219 | `for:${this.connection.peer}`,
220 | );
221 |
222 | let payload: any = {
223 | sdp: offer,
224 | type: this.connection.type,
225 | connectionId: this.connection.connectionId,
226 | metadata: this.connection.metadata,
227 | };
228 |
229 | if (this.connection.type === ConnectionType.Data) {
230 | const dataConnection = (this.connection);
231 |
232 | payload = {
233 | ...payload,
234 | label: dataConnection.label,
235 | reliable: dataConnection.reliable,
236 | serialization: dataConnection.serialization,
237 | };
238 | }
239 |
240 | provider.socket.send({
241 | type: ServerMessageType.Offer,
242 | payload,
243 | dst: this.connection.peer,
244 | });
245 | } catch (err) {
246 | // TODO: investigate why _makeOffer is being called from the answer
247 | if (
248 | err !=
249 | "OperationError: Failed to set local offer sdp: Called in wrong state: kHaveRemoteOffer"
250 | ) {
251 | provider.emitError(PeerErrorType.WebRTC, err);
252 | logger.log("Failed to setLocalDescription, ", err);
253 | }
254 | }
255 | } catch (err_1) {
256 | provider.emitError(PeerErrorType.WebRTC, err_1);
257 | logger.log("Failed to createOffer, ", err_1);
258 | }
259 | }
260 |
261 | private async _makeAnswer(): Promise {
262 | const peerConnection = this.connection.peerConnection;
263 | const provider = this.connection.provider;
264 |
265 | try {
266 | const answer = await peerConnection.createAnswer();
267 | logger.log("Created answer.");
268 |
269 | if (
270 | this.connection.options.sdpTransform &&
271 | typeof this.connection.options.sdpTransform === "function"
272 | ) {
273 | answer.sdp =
274 | this.connection.options.sdpTransform(answer.sdp) || answer.sdp;
275 | }
276 |
277 | try {
278 | await peerConnection.setLocalDescription(answer);
279 |
280 | logger.log(
281 | `Set localDescription:`,
282 | answer,
283 | `for:${this.connection.peer}`,
284 | );
285 |
286 | provider.socket.send({
287 | type: ServerMessageType.Answer,
288 | payload: {
289 | sdp: answer,
290 | type: this.connection.type,
291 | connectionId: this.connection.connectionId,
292 | },
293 | dst: this.connection.peer,
294 | });
295 | } catch (err) {
296 | provider.emitError(PeerErrorType.WebRTC, err);
297 | logger.log("Failed to setLocalDescription, ", err);
298 | }
299 | } catch (err_1) {
300 | provider.emitError(PeerErrorType.WebRTC, err_1);
301 | logger.log("Failed to create answer, ", err_1);
302 | }
303 | }
304 |
305 | /** Handle an SDP. */
306 | async handleSDP(type: string, sdp: any): Promise {
307 | sdp = new RTCSessionDescription(sdp);
308 | const peerConnection = this.connection.peerConnection;
309 | const provider = this.connection.provider;
310 |
311 | logger.log("Setting remote description", sdp);
312 |
313 | const self = this;
314 |
315 | try {
316 | await peerConnection.setRemoteDescription(sdp);
317 | logger.log(`Set remoteDescription:${type} for:${this.connection.peer}`);
318 | if (type === "OFFER") {
319 | await self._makeAnswer();
320 | }
321 | } catch (err) {
322 | provider.emitError(PeerErrorType.WebRTC, err);
323 | logger.log("Failed to setRemoteDescription, ", err);
324 | }
325 | }
326 |
327 | /** Handle a candidate. */
328 | async handleCandidate(ice: RTCIceCandidate) {
329 | logger.log(`handleCandidate:`, ice);
330 |
331 | try {
332 | await this.connection.peerConnection.addIceCandidate(ice);
333 | logger.log(`Added ICE candidate for:${this.connection.peer}`);
334 | } catch (err) {
335 | this.connection.provider.emitError(PeerErrorType.WebRTC, err);
336 | logger.log("Failed to handleCandidate, ", err);
337 | }
338 | }
339 |
340 | private _addTracksToConnection(
341 | stream: MediaStream,
342 | peerConnection: RTCPeerConnection,
343 | ): void {
344 | logger.log(`add tracks from stream ${stream.id} to peer connection`);
345 |
346 | if (!peerConnection.addTrack) {
347 | return logger.error(
348 | `Your browser does't support RTCPeerConnection#addTrack. Ignored.`,
349 | );
350 | }
351 |
352 | stream.getTracks().forEach((track) => {
353 | peerConnection.addTrack(track, stream);
354 | });
355 | }
356 |
357 | private _addStreamToMediaConnection(
358 | stream: MediaStream,
359 | mediaConnection: MediaConnection,
360 | ): void {
361 | logger.log(
362 | `add stream ${stream.id} to media connection ${mediaConnection.connectionId}`,
363 | );
364 |
365 | mediaConnection.addStream(stream);
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/lib/optionInterfaces.ts:
--------------------------------------------------------------------------------
1 | export interface AnswerOption {
2 | /**
3 | * Function which runs before create answer to modify sdp answer message.
4 | */
5 | sdpTransform?: Function;
6 | }
7 |
8 | export interface PeerJSOption {
9 | key?: string;
10 | host?: string;
11 | port?: number;
12 | path?: string;
13 | secure?: boolean;
14 | token?: string;
15 | config?: RTCConfiguration;
16 | debug?: number;
17 | referrerPolicy?: ReferrerPolicy;
18 | }
19 |
20 | export interface PeerConnectOption {
21 | /**
22 | * A unique label by which you want to identify this data connection.
23 | * If left unspecified, a label will be generated at random.
24 | *
25 | * Can be accessed with {@apilink DataConnection.label}
26 | */
27 | label?: string;
28 | /**
29 | * Metadata associated with the connection, passed in by whoever initiated the connection.
30 | *
31 | * Can be accessed with {@apilink DataConnection.metadata}.
32 | * Can be any serializable type.
33 | */
34 | metadata?: any;
35 | serialization?: string;
36 | reliable?: boolean;
37 | }
38 |
39 | export interface CallOption {
40 | /**
41 | * Metadata associated with the connection, passed in by whoever initiated the connection.
42 | *
43 | * Can be accessed with {@apilink MediaConnection.metadata}.
44 | * Can be any serializable type.
45 | */
46 | metadata?: any;
47 | /**
48 | * Function which runs before create offer to modify sdp offer message.
49 | */
50 | sdpTransform?: Function;
51 | }
52 |
--------------------------------------------------------------------------------
/lib/peer.ts:
--------------------------------------------------------------------------------
1 | import { util } from "./util";
2 | import logger, { LogLevel } from "./logger";
3 | import { Socket } from "./socket";
4 | import { MediaConnection } from "./mediaconnection";
5 | import type { DataConnection } from "./dataconnection/DataConnection";
6 | import {
7 | ConnectionType,
8 | PeerErrorType,
9 | ServerMessageType,
10 | SocketEventType,
11 | } from "./enums";
12 | import type { ServerMessage } from "./servermessage";
13 | import { API } from "./api";
14 | import type {
15 | CallOption,
16 | PeerConnectOption,
17 | PeerJSOption,
18 | } from "./optionInterfaces";
19 | import { BinaryPack } from "./dataconnection/BufferedConnection/BinaryPack";
20 | import { Raw } from "./dataconnection/BufferedConnection/Raw";
21 | import { Json } from "./dataconnection/BufferedConnection/Json";
22 |
23 | import { EventEmitterWithError, PeerError } from "./peerError";
24 |
25 | class PeerOptions implements PeerJSOption {
26 | /**
27 | * Prints log messages depending on the debug level passed in.
28 | */
29 | debug?: LogLevel;
30 | /**
31 | * Server host. Defaults to `0.peerjs.com`.
32 | * Also accepts `'/'` to signify relative hostname.
33 | */
34 | host?: string;
35 | /**
36 | * Server port. Defaults to `443`.
37 | */
38 | port?: number;
39 | /**
40 | * The path where your self-hosted PeerServer is running. Defaults to `'/'`
41 | */
42 | path?: string;
43 | /**
44 | * API key for the PeerServer.
45 | * This is not used anymore.
46 | * @deprecated
47 | */
48 | key?: string;
49 | token?: string;
50 | /**
51 | * Configuration hash passed to RTCPeerConnection.
52 | * This hash contains any custom ICE/TURN server configuration.
53 | *
54 | * Defaults to {@apilink util.defaultConfig}
55 | */
56 | config?: any;
57 | /**
58 | * Set to true `true` if you're using TLS.
59 | * :::danger
60 | * If possible *always use TLS*
61 | * :::
62 | */
63 | secure?: boolean;
64 | pingInterval?: number;
65 | referrerPolicy?: ReferrerPolicy;
66 | logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
67 | serializers?: SerializerMapping;
68 | }
69 |
70 | export { type PeerOptions };
71 |
72 | export interface SerializerMapping {
73 | [key: string]: new (
74 | peerId: string,
75 | provider: Peer,
76 | options: any,
77 | ) => DataConnection;
78 | }
79 |
80 | export interface PeerEvents {
81 | /**
82 | * Emitted when a connection to the PeerServer is established.
83 | *
84 | * You may use the peer before this is emitted, but messages to the server will be queued. id
is the brokering ID of the peer (which was either provided in the constructor or assigned by the server).You should not wait for this event before connecting to other peers if connection speed is important.
85 | */
86 | open: (id: string) => void;
87 | /**
88 | * Emitted when a new data connection is established from a remote peer.
89 | */
90 | connection: (dataConnection: DataConnection) => void;
91 | /**
92 | * Emitted when a remote peer attempts to call you.
93 | */
94 | call: (mediaConnection: MediaConnection) => void;
95 | /**
96 | * Emitted when the peer is destroyed and can no longer accept or create any new connections.
97 | */
98 | close: () => void;
99 | /**
100 | * Emitted when the peer is disconnected from the signalling server
101 | */
102 | disconnected: (currentId: string) => void;
103 | /**
104 | * Errors on the peer are almost always fatal and will destroy the peer.
105 | *
106 | * Errors from the underlying socket and PeerConnections are forwarded here.
107 | */
108 | error: (error: PeerError<`${PeerErrorType}`>) => void;
109 | }
110 | /**
111 | * A peer who can initiate connections with other peers.
112 | */
113 | export class Peer extends EventEmitterWithError {
114 | private static readonly DEFAULT_KEY = "peerjs";
115 |
116 | protected readonly _serializers: SerializerMapping = {
117 | raw: Raw,
118 | json: Json,
119 | binary: BinaryPack,
120 | "binary-utf8": BinaryPack,
121 |
122 | default: BinaryPack,
123 | };
124 | private readonly _options: PeerOptions;
125 | private readonly _api: API;
126 | private readonly _socket: Socket;
127 |
128 | private _id: string | null = null;
129 | private _lastServerId: string | null = null;
130 |
131 | // States.
132 | private _destroyed = false; // Connections have been killed
133 | private _disconnected = false; // Connection to PeerServer killed but P2P connections still active
134 | private _open = false; // Sockets and such are not yet open.
135 | private readonly _connections: Map<
136 | string,
137 | (DataConnection | MediaConnection)[]
138 | > = new Map(); // All connections for this peer.
139 | private readonly _lostMessages: Map = new Map(); // src => [list of messages]
140 | /**
141 | * The brokering ID of this peer
142 | *
143 | * If no ID was specified in {@apilink Peer | the constructor},
144 | * this will be `undefined` until the {@apilink PeerEvents | `open`} event is emitted.
145 | */
146 | get id() {
147 | return this._id;
148 | }
149 |
150 | get options() {
151 | return this._options;
152 | }
153 |
154 | get open() {
155 | return this._open;
156 | }
157 |
158 | /**
159 | * @internal
160 | */
161 | get socket() {
162 | return this._socket;
163 | }
164 |
165 | /**
166 | * A hash of all connections associated with this peer, keyed by the remote peer's ID.
167 | * @deprecated
168 | * Return type will change from Object to Map
169 | */
170 | get connections(): Object {
171 | const plainConnections = Object.create(null);
172 |
173 | for (const [k, v] of this._connections) {
174 | plainConnections[k] = v;
175 | }
176 |
177 | return plainConnections;
178 | }
179 |
180 | /**
181 | * true if this peer and all of its connections can no longer be used.
182 | */
183 | get destroyed() {
184 | return this._destroyed;
185 | }
186 | /**
187 | * false if there is an active connection to the PeerServer.
188 | */
189 | get disconnected() {
190 | return this._disconnected;
191 | }
192 |
193 | /**
194 | * A peer can connect to other peers and listen for connections.
195 | */
196 | constructor();
197 |
198 | /**
199 | * A peer can connect to other peers and listen for connections.
200 | * @param options for specifying details about PeerServer
201 | */
202 | constructor(options: PeerOptions);
203 |
204 | /**
205 | * A peer can connect to other peers and listen for connections.
206 | * @param id Other peers can connect to this peer using the provided ID.
207 | * If no ID is given, one will be generated by the brokering server.
208 | * The ID must start and end with an alphanumeric character (lower or upper case character or a digit). In the middle of the ID spaces, dashes (-) and underscores (_) are allowed. Use {@apilink PeerOptions.metadata } to send identifying information.
209 | * @param options for specifying details about PeerServer
210 | */
211 | constructor(id: string, options?: PeerOptions);
212 |
213 | constructor(id?: string | PeerOptions, options?: PeerOptions) {
214 | super();
215 |
216 | let userId: string | undefined;
217 |
218 | // Deal with overloading
219 | if (id && id.constructor == Object) {
220 | options = id as PeerOptions;
221 | } else if (id) {
222 | userId = id.toString();
223 | }
224 |
225 | // Configurize options
226 | options = {
227 | debug: 0, // 1: Errors, 2: Warnings, 3: All logs
228 | host: util.CLOUD_HOST,
229 | port: util.CLOUD_PORT,
230 | path: "/",
231 | key: Peer.DEFAULT_KEY,
232 | token: util.randomToken(),
233 | config: util.defaultConfig,
234 | referrerPolicy: "strict-origin-when-cross-origin",
235 | serializers: {},
236 | ...options,
237 | };
238 | this._options = options;
239 | this._serializers = { ...this._serializers, ...this.options.serializers };
240 |
241 | // Detect relative URL host.
242 | if (this._options.host === "/") {
243 | this._options.host = window.location.hostname;
244 | }
245 |
246 | // Set path correctly.
247 | if (this._options.path) {
248 | if (this._options.path[0] !== "/") {
249 | this._options.path = "/" + this._options.path;
250 | }
251 | if (this._options.path[this._options.path.length - 1] !== "/") {
252 | this._options.path += "/";
253 | }
254 | }
255 |
256 | // Set whether we use SSL to same as current host
257 | if (
258 | this._options.secure === undefined &&
259 | this._options.host !== util.CLOUD_HOST
260 | ) {
261 | this._options.secure = util.isSecure();
262 | } else if (this._options.host == util.CLOUD_HOST) {
263 | this._options.secure = true;
264 | }
265 | // Set a custom log function if present
266 | if (this._options.logFunction) {
267 | logger.setLogFunction(this._options.logFunction);
268 | }
269 |
270 | logger.logLevel = this._options.debug || 0;
271 |
272 | this._api = new API(options);
273 | this._socket = this._createServerConnection();
274 |
275 | // Sanity checks
276 | // Ensure WebRTC supported
277 | if (!util.supports.audioVideo && !util.supports.data) {
278 | this._delayedAbort(
279 | PeerErrorType.BrowserIncompatible,
280 | "The current browser does not support WebRTC",
281 | );
282 | return;
283 | }
284 |
285 | // Ensure alphanumeric id
286 | if (!!userId && !util.validateId(userId)) {
287 | this._delayedAbort(PeerErrorType.InvalidID, `ID "${userId}" is invalid`);
288 | return;
289 | }
290 |
291 | if (userId) {
292 | this._initialize(userId);
293 | } else {
294 | this._api
295 | .retrieveId()
296 | .then((id) => this._initialize(id))
297 | .catch((error) => this._abort(PeerErrorType.ServerError, error));
298 | }
299 | }
300 |
301 | private _createServerConnection(): Socket {
302 | const socket = new Socket(
303 | this._options.secure,
304 | this._options.host!,
305 | this._options.port!,
306 | this._options.path!,
307 | this._options.key!,
308 | this._options.pingInterval,
309 | );
310 |
311 | socket.on(SocketEventType.Message, (data: ServerMessage) => {
312 | this._handleMessage(data);
313 | });
314 |
315 | socket.on(SocketEventType.Error, (error: string) => {
316 | this._abort(PeerErrorType.SocketError, error);
317 | });
318 |
319 | socket.on(SocketEventType.Disconnected, () => {
320 | if (this.disconnected) {
321 | return;
322 | }
323 |
324 | this.emitError(PeerErrorType.Network, "Lost connection to server.");
325 | this.disconnect();
326 | });
327 |
328 | socket.on(SocketEventType.Close, () => {
329 | if (this.disconnected) {
330 | return;
331 | }
332 |
333 | this._abort(
334 | PeerErrorType.SocketClosed,
335 | "Underlying socket is already closed.",
336 | );
337 | });
338 |
339 | return socket;
340 | }
341 |
342 | /** Initialize a connection with the server. */
343 | private _initialize(id: string): void {
344 | this._id = id;
345 | this.socket.start(id, this._options.token!);
346 | }
347 |
348 | /** Handles messages from the server. */
349 | private _handleMessage(message: ServerMessage): void {
350 | const type = message.type;
351 | const payload = message.payload;
352 | const peerId = message.src;
353 |
354 | switch (type) {
355 | case ServerMessageType.Open: // The connection to the server is open.
356 | this._lastServerId = this.id;
357 | this._open = true;
358 | this.emit("open", this.id);
359 | break;
360 | case ServerMessageType.Error: // Server error.
361 | this._abort(PeerErrorType.ServerError, payload.msg);
362 | break;
363 | case ServerMessageType.IdTaken: // The selected ID is taken.
364 | this._abort(PeerErrorType.UnavailableID, `ID "${this.id}" is taken`);
365 | break;
366 | case ServerMessageType.InvalidKey: // The given API key cannot be found.
367 | this._abort(
368 | PeerErrorType.InvalidKey,
369 | `API KEY "${this._options.key}" is invalid`,
370 | );
371 | break;
372 | case ServerMessageType.Leave: // Another peer has closed its connection to this peer.
373 | logger.log(`Received leave message from ${peerId}`);
374 | this._cleanupPeer(peerId);
375 | this._connections.delete(peerId);
376 | break;
377 | case ServerMessageType.Expire: // The offer sent to a peer has expired without response.
378 | this.emitError(
379 | PeerErrorType.PeerUnavailable,
380 | `Could not connect to peer ${peerId}`,
381 | );
382 | break;
383 | case ServerMessageType.Offer: {
384 | // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
385 | const connectionId = payload.connectionId;
386 | let connection = this.getConnection(peerId, connectionId);
387 |
388 | if (connection) {
389 | connection.close();
390 | logger.warn(
391 | `Offer received for existing Connection ID:${connectionId}`,
392 | );
393 | }
394 |
395 | // Create a new connection.
396 | if (payload.type === ConnectionType.Media) {
397 | const mediaConnection = new MediaConnection(peerId, this, {
398 | connectionId: connectionId,
399 | _payload: payload,
400 | metadata: payload.metadata,
401 | });
402 | connection = mediaConnection;
403 | this._addConnection(peerId, connection);
404 | this.emit("call", mediaConnection);
405 | } else if (payload.type === ConnectionType.Data) {
406 | const dataConnection = new this._serializers[payload.serialization](
407 | peerId,
408 | this,
409 | {
410 | connectionId: connectionId,
411 | _payload: payload,
412 | metadata: payload.metadata,
413 | label: payload.label,
414 | serialization: payload.serialization,
415 | reliable: payload.reliable,
416 | },
417 | );
418 | connection = dataConnection;
419 |
420 | this._addConnection(peerId, connection);
421 | this.emit("connection", dataConnection);
422 | } else {
423 | logger.warn(`Received malformed connection type:${payload.type}`);
424 | return;
425 | }
426 |
427 | // Find messages.
428 | const messages = this._getMessages(connectionId);
429 | for (const message of messages) {
430 | connection.handleMessage(message);
431 | }
432 |
433 | break;
434 | }
435 | default: {
436 | if (!payload) {
437 | logger.warn(
438 | `You received a malformed message from ${peerId} of type ${type}`,
439 | );
440 | return;
441 | }
442 |
443 | const connectionId = payload.connectionId;
444 | const connection = this.getConnection(peerId, connectionId);
445 |
446 | if (connection && connection.peerConnection) {
447 | // Pass it on.
448 | connection.handleMessage(message);
449 | } else if (connectionId) {
450 | // Store for possible later use
451 | this._storeMessage(connectionId, message);
452 | } else {
453 | logger.warn("You received an unrecognized message:", message);
454 | }
455 | break;
456 | }
457 | }
458 | }
459 |
460 | /** Stores messages without a set up connection, to be claimed later. */
461 | private _storeMessage(connectionId: string, message: ServerMessage): void {
462 | if (!this._lostMessages.has(connectionId)) {
463 | this._lostMessages.set(connectionId, []);
464 | }
465 |
466 | this._lostMessages.get(connectionId).push(message);
467 | }
468 |
469 | /**
470 | * Retrieve messages from lost message store
471 | * @internal
472 | */
473 | //TODO Change it to private
474 | public _getMessages(connectionId: string): ServerMessage[] {
475 | const messages = this._lostMessages.get(connectionId);
476 |
477 | if (messages) {
478 | this._lostMessages.delete(connectionId);
479 | return messages;
480 | }
481 |
482 | return [];
483 | }
484 |
485 | /**
486 | * Connects to the remote peer specified by id and returns a data connection.
487 | * @param peer The brokering ID of the remote peer (their {@apilink Peer.id}).
488 | * @param options for specifying details about Peer Connection
489 | */
490 | connect(peer: string, options: PeerConnectOption = {}): DataConnection {
491 | options = {
492 | serialization: "default",
493 | ...options,
494 | };
495 | if (this.disconnected) {
496 | logger.warn(
497 | "You cannot connect to a new Peer because you called " +
498 | ".disconnect() on this Peer and ended your connection with the " +
499 | "server. You can create a new Peer to reconnect, or call reconnect " +
500 | "on this peer if you believe its ID to still be available.",
501 | );
502 | this.emitError(
503 | PeerErrorType.Disconnected,
504 | "Cannot connect to new Peer after disconnecting from server.",
505 | );
506 | return;
507 | }
508 |
509 | const dataConnection = new this._serializers[options.serialization](
510 | peer,
511 | this,
512 | options,
513 | );
514 | this._addConnection(peer, dataConnection);
515 | return dataConnection;
516 | }
517 |
518 | /**
519 | * Calls the remote peer specified by id and returns a media connection.
520 | * @param peer The brokering ID of the remote peer (their peer.id).
521 | * @param stream The caller's media stream
522 | * @param options Metadata associated with the connection, passed in by whoever initiated the connection.
523 | */
524 | call(
525 | peer: string,
526 | stream: MediaStream,
527 | options: CallOption = {},
528 | ): MediaConnection {
529 | if (this.disconnected) {
530 | logger.warn(
531 | "You cannot connect to a new Peer because you called " +
532 | ".disconnect() on this Peer and ended your connection with the " +
533 | "server. You can create a new Peer to reconnect.",
534 | );
535 | this.emitError(
536 | PeerErrorType.Disconnected,
537 | "Cannot connect to new Peer after disconnecting from server.",
538 | );
539 | return;
540 | }
541 |
542 | if (!stream) {
543 | logger.error(
544 | "To call a peer, you must provide a stream from your browser's `getUserMedia`.",
545 | );
546 | return;
547 | }
548 |
549 | const mediaConnection = new MediaConnection(peer, this, {
550 | ...options,
551 | _stream: stream,
552 | });
553 | this._addConnection(peer, mediaConnection);
554 | return mediaConnection;
555 | }
556 |
557 | /** Add a data/media connection to this peer. */
558 | private _addConnection(
559 | peerId: string,
560 | connection: MediaConnection | DataConnection,
561 | ): void {
562 | logger.log(
563 | `add connection ${connection.type}:${connection.connectionId} to peerId:${peerId}`,
564 | );
565 |
566 | if (!this._connections.has(peerId)) {
567 | this._connections.set(peerId, []);
568 | }
569 | this._connections.get(peerId).push(connection);
570 | }
571 |
572 | //TODO should be private
573 | _removeConnection(connection: DataConnection | MediaConnection): void {
574 | const connections = this._connections.get(connection.peer);
575 |
576 | if (connections) {
577 | const index = connections.indexOf(connection);
578 |
579 | if (index !== -1) {
580 | connections.splice(index, 1);
581 | }
582 | }
583 |
584 | //remove from lost messages
585 | this._lostMessages.delete(connection.connectionId);
586 | }
587 |
588 | /** Retrieve a data/media connection for this peer. */
589 | getConnection(
590 | peerId: string,
591 | connectionId: string,
592 | ): null | DataConnection | MediaConnection {
593 | const connections = this._connections.get(peerId);
594 | if (!connections) {
595 | return null;
596 | }
597 |
598 | for (const connection of connections) {
599 | if (connection.connectionId === connectionId) {
600 | return connection;
601 | }
602 | }
603 |
604 | return null;
605 | }
606 |
607 | private _delayedAbort(type: PeerErrorType, message: string | Error): void {
608 | setTimeout(() => {
609 | this._abort(type, message);
610 | }, 0);
611 | }
612 |
613 | /**
614 | * Emits an error message and destroys the Peer.
615 | * The Peer is not destroyed if it's in a disconnected state, in which case
616 | * it retains its disconnected state and its existing connections.
617 | */
618 | private _abort(type: PeerErrorType, message: string | Error): void {
619 | logger.error("Aborting!");
620 |
621 | this.emitError(type, message);
622 |
623 | if (!this._lastServerId) {
624 | this.destroy();
625 | } else {
626 | this.disconnect();
627 | }
628 | }
629 |
630 | /**
631 | * Destroys the Peer: closes all active connections as well as the connection
632 | * to the server.
633 | *
634 | * :::caution
635 | * This cannot be undone; the respective peer object will no longer be able
636 | * to create or receive any connections, its ID will be forfeited on the server,
637 | * and all of its data and media connections will be closed.
638 | * :::
639 | */
640 | destroy(): void {
641 | if (this.destroyed) {
642 | return;
643 | }
644 |
645 | logger.log(`Destroy peer with ID:${this.id}`);
646 |
647 | this.disconnect();
648 | this._cleanup();
649 |
650 | this._destroyed = true;
651 |
652 | this.emit("close");
653 | }
654 |
655 | /** Disconnects every connection on this peer. */
656 | private _cleanup(): void {
657 | for (const peerId of this._connections.keys()) {
658 | this._cleanupPeer(peerId);
659 | this._connections.delete(peerId);
660 | }
661 |
662 | this.socket.removeAllListeners();
663 | }
664 |
665 | /** Closes all connections to this peer. */
666 | private _cleanupPeer(peerId: string): void {
667 | const connections = this._connections.get(peerId);
668 |
669 | if (!connections) return;
670 |
671 | for (const connection of connections) {
672 | connection.close();
673 | }
674 | }
675 |
676 | /**
677 | * Disconnects the Peer's connection to the PeerServer. Does not close any
678 | * active connections.
679 | * Warning: The peer can no longer create or accept connections after being
680 | * disconnected. It also cannot reconnect to the server.
681 | */
682 | disconnect(): void {
683 | if (this.disconnected) {
684 | return;
685 | }
686 |
687 | const currentId = this.id;
688 |
689 | logger.log(`Disconnect peer with ID:${currentId}`);
690 |
691 | this._disconnected = true;
692 | this._open = false;
693 |
694 | this.socket.close();
695 |
696 | this._lastServerId = currentId;
697 | this._id = null;
698 |
699 | this.emit("disconnected", currentId);
700 | }
701 |
702 | /** Attempts to reconnect with the same ID.
703 | *
704 | * Only {@apilink Peer.disconnect | disconnected peers} can be reconnected.
705 | * Destroyed peers cannot be reconnected.
706 | * If the connection fails (as an example, if the peer's old ID is now taken),
707 | * the peer's existing connections will not close, but any associated errors events will fire.
708 | */
709 | reconnect(): void {
710 | if (this.disconnected && !this.destroyed) {
711 | logger.log(
712 | `Attempting reconnection to server with ID ${this._lastServerId}`,
713 | );
714 | this._disconnected = false;
715 | this._initialize(this._lastServerId!);
716 | } else if (this.destroyed) {
717 | throw new Error(
718 | "This peer cannot reconnect to the server. It has already been destroyed.",
719 | );
720 | } else if (!this.disconnected && !this.open) {
721 | // Do nothing. We're still connecting the first time.
722 | logger.error(
723 | "In a hurry? We're still trying to make the initial connection!",
724 | );
725 | } else {
726 | throw new Error(
727 | `Peer ${this.id} cannot reconnect because it is not disconnected from the server!`,
728 | );
729 | }
730 | }
731 |
732 | /**
733 | * Get a list of available peer IDs. If you're running your own server, you'll
734 | * want to set allow_discovery: true in the PeerServer options. If you're using
735 | * the cloud server, email team@peerjs.com to get the functionality enabled for
736 | * your key.
737 | */
738 | listAllPeers(cb = (_: any[]) => {}): void {
739 | this._api
740 | .listAllPeers()
741 | .then((peers) => cb(peers))
742 | .catch((error) => this._abort(PeerErrorType.ServerError, error));
743 | }
744 | }
745 |
--------------------------------------------------------------------------------
/lib/peerError.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "eventemitter3";
2 | import logger from "./logger";
3 |
4 | export interface EventsWithError {
5 | error: (error: PeerError<`${ErrorType}`>) => void;
6 | }
7 |
8 | export class EventEmitterWithError<
9 | ErrorType extends string,
10 | Events extends EventsWithError,
11 | > extends EventEmitter {
12 | /**
13 | * Emits a typed error message.
14 | *
15 | * @internal
16 | */
17 | emitError(type: ErrorType, err: string | Error): void {
18 | logger.error("Error:", err);
19 |
20 | // @ts-ignore
21 | this.emit("error", new PeerError<`${ErrorType}`>(`${type}`, err));
22 | }
23 | }
24 | /**
25 | * A PeerError is emitted whenever an error occurs.
26 | * It always has a `.type`, which can be used to identify the error.
27 | */
28 | export class PeerError extends Error {
29 | /**
30 | * @internal
31 | */
32 | constructor(type: T, err: Error | string) {
33 | if (typeof err === "string") {
34 | super(err);
35 | } else {
36 | super();
37 | Object.assign(this, err);
38 | }
39 |
40 | this.type = type;
41 | }
42 |
43 | public type: T;
44 | }
45 |
--------------------------------------------------------------------------------
/lib/servermessage.ts:
--------------------------------------------------------------------------------
1 | import type { ServerMessageType } from "./enums";
2 |
3 | export class ServerMessage {
4 | type: ServerMessageType;
5 | payload: any;
6 | src: string;
7 | }
8 |
--------------------------------------------------------------------------------
/lib/socket.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "eventemitter3";
2 | import logger from "./logger";
3 | import { ServerMessageType, SocketEventType } from "./enums";
4 | import { version } from "../package.json";
5 |
6 | /**
7 | * An abstraction on top of WebSockets to provide fastest
8 | * possible connection for peers.
9 | */
10 | export class Socket extends EventEmitter {
11 | private _disconnected: boolean = true;
12 | private _id?: string;
13 | private _messagesQueue: Array = [];
14 | private _socket?: WebSocket;
15 | private _wsPingTimer?: any;
16 | private readonly _baseUrl: string;
17 |
18 | constructor(
19 | secure: any,
20 | host: string,
21 | port: number,
22 | path: string,
23 | key: string,
24 | private readonly pingInterval: number = 5000,
25 | ) {
26 | super();
27 |
28 | const wsProtocol = secure ? "wss://" : "ws://";
29 |
30 | this._baseUrl = wsProtocol + host + ":" + port + path + "peerjs?key=" + key;
31 | }
32 |
33 | start(id: string, token: string): void {
34 | this._id = id;
35 |
36 | const wsUrl = `${this._baseUrl}&id=${id}&token=${token}`;
37 |
38 | if (!!this._socket || !this._disconnected) {
39 | return;
40 | }
41 |
42 | this._socket = new WebSocket(wsUrl + "&version=" + version);
43 | this._disconnected = false;
44 |
45 | this._socket.onmessage = (event) => {
46 | let data;
47 |
48 | try {
49 | data = JSON.parse(event.data);
50 | logger.log("Server message received:", data);
51 | } catch (e) {
52 | logger.log("Invalid server message", event.data);
53 | return;
54 | }
55 |
56 | this.emit(SocketEventType.Message, data);
57 | };
58 |
59 | this._socket.onclose = (event) => {
60 | if (this._disconnected) {
61 | return;
62 | }
63 |
64 | logger.log("Socket closed.", event);
65 |
66 | this._cleanup();
67 | this._disconnected = true;
68 |
69 | this.emit(SocketEventType.Disconnected);
70 | };
71 |
72 | // Take care of the queue of connections if necessary and make sure Peer knows
73 | // socket is open.
74 | this._socket.onopen = () => {
75 | if (this._disconnected) {
76 | return;
77 | }
78 |
79 | this._sendQueuedMessages();
80 |
81 | logger.log("Socket open");
82 |
83 | this._scheduleHeartbeat();
84 | };
85 | }
86 |
87 | private _scheduleHeartbeat(): void {
88 | this._wsPingTimer = setTimeout(() => {
89 | this._sendHeartbeat();
90 | }, this.pingInterval);
91 | }
92 |
93 | private _sendHeartbeat(): void {
94 | if (!this._wsOpen()) {
95 | logger.log(`Cannot send heartbeat, because socket closed`);
96 | return;
97 | }
98 |
99 | const message = JSON.stringify({ type: ServerMessageType.Heartbeat });
100 |
101 | this._socket!.send(message);
102 |
103 | this._scheduleHeartbeat();
104 | }
105 |
106 | /** Is the websocket currently open? */
107 | private _wsOpen(): boolean {
108 | return !!this._socket && this._socket.readyState === 1;
109 | }
110 |
111 | /** Send queued messages. */
112 | private _sendQueuedMessages(): void {
113 | //Create copy of queue and clear it,
114 | //because send method push the message back to queue if smth will go wrong
115 | const copiedQueue = [...this._messagesQueue];
116 | this._messagesQueue = [];
117 |
118 | for (const message of copiedQueue) {
119 | this.send(message);
120 | }
121 | }
122 |
123 | /** Exposed send for DC & Peer. */
124 | send(data: any): void {
125 | if (this._disconnected) {
126 | return;
127 | }
128 |
129 | // If we didn't get an ID yet, we can't yet send anything so we should queue
130 | // up these messages.
131 | if (!this._id) {
132 | this._messagesQueue.push(data);
133 | return;
134 | }
135 |
136 | if (!data.type) {
137 | this.emit(SocketEventType.Error, "Invalid message");
138 | return;
139 | }
140 |
141 | if (!this._wsOpen()) {
142 | return;
143 | }
144 |
145 | const message = JSON.stringify(data);
146 |
147 | this._socket!.send(message);
148 | }
149 |
150 | close(): void {
151 | if (this._disconnected) {
152 | return;
153 | }
154 |
155 | this._cleanup();
156 |
157 | this._disconnected = true;
158 | }
159 |
160 | private _cleanup(): void {
161 | if (this._socket) {
162 | this._socket.onopen =
163 | this._socket.onmessage =
164 | this._socket.onclose =
165 | null;
166 | this._socket.close();
167 | this._socket = undefined;
168 | }
169 |
170 | clearTimeout(this._wsPingTimer!);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/lib/supports.ts:
--------------------------------------------------------------------------------
1 | import webRTCAdapter_import from "webrtc-adapter";
2 |
3 | const webRTCAdapter: typeof webRTCAdapter_import =
4 | //@ts-ignore
5 | webRTCAdapter_import.default || webRTCAdapter_import;
6 |
7 | export const Supports = new (class {
8 | readonly isIOS =
9 | typeof navigator !== "undefined"
10 | ? ["iPad", "iPhone", "iPod"].includes(navigator.platform)
11 | : false;
12 | readonly supportedBrowsers = ["firefox", "chrome", "safari"];
13 |
14 | readonly minFirefoxVersion = 59;
15 | readonly minChromeVersion = 72;
16 | readonly minSafariVersion = 605;
17 |
18 | isWebRTCSupported(): boolean {
19 | return typeof RTCPeerConnection !== "undefined";
20 | }
21 |
22 | isBrowserSupported(): boolean {
23 | const browser = this.getBrowser();
24 | const version = this.getVersion();
25 |
26 | const validBrowser = this.supportedBrowsers.includes(browser);
27 |
28 | if (!validBrowser) return false;
29 |
30 | if (browser === "chrome") return version >= this.minChromeVersion;
31 | if (browser === "firefox") return version >= this.minFirefoxVersion;
32 | if (browser === "safari")
33 | return !this.isIOS && version >= this.minSafariVersion;
34 |
35 | return false;
36 | }
37 |
38 | getBrowser(): string {
39 | return webRTCAdapter.browserDetails.browser;
40 | }
41 |
42 | getVersion(): number {
43 | return webRTCAdapter.browserDetails.version || 0;
44 | }
45 |
46 | isUnifiedPlanSupported(): boolean {
47 | const browser = this.getBrowser();
48 | const version = webRTCAdapter.browserDetails.version || 0;
49 |
50 | if (browser === "chrome" && version < this.minChromeVersion) return false;
51 | if (browser === "firefox" && version >= this.minFirefoxVersion) return true;
52 | if (
53 | !window.RTCRtpTransceiver ||
54 | !("currentDirection" in RTCRtpTransceiver.prototype)
55 | )
56 | return false;
57 |
58 | let tempPc: RTCPeerConnection;
59 | let supported = false;
60 |
61 | try {
62 | tempPc = new RTCPeerConnection();
63 | tempPc.addTransceiver("audio");
64 | supported = true;
65 | } catch (e) {
66 | } finally {
67 | if (tempPc) {
68 | tempPc.close();
69 | }
70 | }
71 |
72 | return supported;
73 | }
74 |
75 | toString(): string {
76 | return `Supports:
77 | browser:${this.getBrowser()}
78 | version:${this.getVersion()}
79 | isIOS:${this.isIOS}
80 | isWebRTCSupported:${this.isWebRTCSupported()}
81 | isBrowserSupported:${this.isBrowserSupported()}
82 | isUnifiedPlanSupported:${this.isUnifiedPlanSupported()}`;
83 | }
84 | })();
85 |
--------------------------------------------------------------------------------
/lib/util.ts:
--------------------------------------------------------------------------------
1 | import { BinaryPackChunker } from "./dataconnection/BufferedConnection/binaryPackChunker";
2 | import * as BinaryPack from "peerjs-js-binarypack";
3 | import { Supports } from "./supports";
4 | import { validateId } from "./utils/validateId";
5 | import { randomToken } from "./utils/randomToken";
6 |
7 | export interface UtilSupportsObj {
8 | /**
9 | * The current browser.
10 | * This property can be useful in determining whether two peers can connect.
11 | *
12 | * ```ts
13 | * if (util.browser === 'firefox') {
14 | * // OK to peer with Firefox peers.
15 | * }
16 | * ```
17 | *
18 | * `util.browser` can currently have the values
19 | * `'firefox', 'chrome', 'safari', 'edge', 'Not a supported browser.', 'Not a browser.' (unknown WebRTC-compatible agent).
20 | */
21 | browser: boolean;
22 | webRTC: boolean;
23 | /**
24 | * True if the current browser supports media streams and PeerConnection.
25 | */
26 | audioVideo: boolean;
27 | /**
28 | * True if the current browser supports DataChannel and PeerConnection.
29 | */
30 | data: boolean;
31 | binaryBlob: boolean;
32 | /**
33 | * True if the current browser supports reliable DataChannels.
34 | */
35 | reliable: boolean;
36 | }
37 |
38 | const DEFAULT_CONFIG = {
39 | iceServers: [
40 | { urls: "stun:stun.l.google.com:19302" },
41 | {
42 | urls: [
43 | "turn:eu-0.turn.peerjs.com:3478",
44 | "turn:us-0.turn.peerjs.com:3478",
45 | ],
46 | username: "peerjs",
47 | credential: "peerjsp",
48 | },
49 | ],
50 | sdpSemantics: "unified-plan",
51 | };
52 |
53 | export class Util extends BinaryPackChunker {
54 | noop(): void {}
55 |
56 | readonly CLOUD_HOST = "0.peerjs.com";
57 | readonly CLOUD_PORT = 443;
58 |
59 | // Browsers that need chunking:
60 | readonly chunkedBrowsers = { Chrome: 1, chrome: 1 };
61 |
62 | // Returns browser-agnostic default config
63 | readonly defaultConfig = DEFAULT_CONFIG;
64 |
65 | readonly browser = Supports.getBrowser();
66 | readonly browserVersion = Supports.getVersion();
67 |
68 | pack = BinaryPack.pack;
69 | unpack = BinaryPack.unpack;
70 |
71 | /**
72 | * A hash of WebRTC features mapped to booleans that correspond to whether the feature is supported by the current browser.
73 | *
74 | * :::caution
75 | * Only the properties documented here are guaranteed to be present on `util.supports`
76 | * :::
77 | */
78 | readonly supports = (function () {
79 | const supported: UtilSupportsObj = {
80 | browser: Supports.isBrowserSupported(),
81 | webRTC: Supports.isWebRTCSupported(),
82 | audioVideo: false,
83 | data: false,
84 | binaryBlob: false,
85 | reliable: false,
86 | };
87 |
88 | if (!supported.webRTC) return supported;
89 |
90 | let pc: RTCPeerConnection;
91 |
92 | try {
93 | pc = new RTCPeerConnection(DEFAULT_CONFIG);
94 |
95 | supported.audioVideo = true;
96 |
97 | let dc: RTCDataChannel;
98 |
99 | try {
100 | dc = pc.createDataChannel("_PEERJSTEST", { ordered: true });
101 | supported.data = true;
102 | supported.reliable = !!dc.ordered;
103 |
104 | // Binary test
105 | try {
106 | dc.binaryType = "blob";
107 | supported.binaryBlob = !Supports.isIOS;
108 | } catch (e) {}
109 | } catch (e) {
110 | } finally {
111 | if (dc) {
112 | dc.close();
113 | }
114 | }
115 | } catch (e) {
116 | } finally {
117 | if (pc) {
118 | pc.close();
119 | }
120 | }
121 |
122 | return supported;
123 | })();
124 |
125 | // Ensure alphanumeric ids
126 | validateId = validateId;
127 | randomToken = randomToken;
128 |
129 | blobToArrayBuffer(
130 | blob: Blob,
131 | cb: (arg: ArrayBuffer | null) => void,
132 | ): FileReader {
133 | const fr = new FileReader();
134 |
135 | fr.onload = function (evt) {
136 | if (evt.target) {
137 | cb(evt.target.result as ArrayBuffer);
138 | }
139 | };
140 |
141 | fr.readAsArrayBuffer(blob);
142 |
143 | return fr;
144 | }
145 |
146 | binaryStringToArrayBuffer(binary: string): ArrayBuffer | SharedArrayBuffer {
147 | const byteArray = new Uint8Array(binary.length);
148 |
149 | for (let i = 0; i < binary.length; i++) {
150 | byteArray[i] = binary.charCodeAt(i) & 0xff;
151 | }
152 |
153 | return byteArray.buffer;
154 | }
155 | isSecure(): boolean {
156 | return location.protocol === "https:";
157 | }
158 | }
159 |
160 | /**
161 | * Provides a variety of helpful utilities.
162 | *
163 | * :::caution
164 | * Only the utilities documented here are guaranteed to be present on `util`.
165 | * Undocumented utilities can be removed without warning.
166 | * We don't consider these to be breaking changes.
167 | * :::
168 | */
169 | export const util = new Util();
170 |
--------------------------------------------------------------------------------
/lib/utils/randomToken.ts:
--------------------------------------------------------------------------------
1 | export const randomToken = () => Math.random().toString(36).slice(2);
2 |
--------------------------------------------------------------------------------
/lib/utils/validateId.ts:
--------------------------------------------------------------------------------
1 | export const validateId = (id: string): boolean => {
2 | // Allow empty ids
3 | return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.test(id);
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "peerjs",
3 | "version": "1.5.4",
4 | "keywords": [
5 | "peerjs",
6 | "webrtc",
7 | "p2p",
8 | "rtc"
9 | ],
10 | "description": "PeerJS client",
11 | "homepage": "https://peerjs.com",
12 | "bugs": {
13 | "url": "https://github.com/peers/peerjs/issues"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/peers/peerjs"
18 | },
19 | "license": "MIT",
20 | "contributors": [
21 | "Michelle Bu ",
22 | "afrokick ",
23 | "ericz ",
24 | "Jairo ",
25 | "Jonas Gloning <34194370+jonasgloning@users.noreply.github.com>",
26 | "Jairo Caro-Accino Viciana ",
27 | "Carlos Caballero ",
28 | "hc ",
29 | "Muhammad Asif ",
30 | "PrashoonB ",
31 | "Harsh Bardhan Mishra <47351025+HarshCasper@users.noreply.github.com>",
32 | "akotynski ",
33 | "lmb ",
34 | "Jairooo ",
35 | "Moritz Stückler ",
36 | "Simon ",
37 | "Denis Lukov ",
38 | "Philipp Hancke ",
39 | "Hans Oksendahl ",
40 | "Jess ",
41 | "khankuan ",
42 | "DUODVK ",
43 | "XiZhao ",
44 | "Matthias Lohr ",
45 | "=frank tree <=frnktrb@googlemail.com>",
46 | "Andre Eckardt ",
47 | "Chris Cowan ",
48 | "Alex Chuev ",
49 | "alxnull ",
50 | "Yemel Jardi ",
51 | "Ben Parnell ",
52 | "Benny Lichtner ",
53 | "fresheneesz ",
54 | "bob.barstead@exaptive.com ",
55 | "chandika ",
56 | "emersion ",
57 | "Christopher Van ",
58 | "eddieherm ",
59 | "Eduardo Pinho ",
60 | "Evandro Zanatta ",
61 | "Gardner Bickford ",
62 | "Gian Luca ",
63 | "PatrickJS ",
64 | "jonnyf ",
65 | "Hizkia Felix ",
66 | "Hristo Oskov ",
67 | "Isaac Madwed ",
68 | "Ilya Konanykhin ",
69 | "jasonbarry ",
70 | "Jonathan Burke ",
71 | "Josh Hamit ",
72 | "Jordan Austin ",
73 | "Joel Wetzell ",
74 | "xizhao ",
75 | "Alberto Torres ",
76 | "Jonathan Mayol ",
77 | "Jefferson Felix ",
78 | "Rolf Erik Lekang ",
79 | "Kevin Mai-Husan Chia ",
80 | "Pepijn de Vos ",
81 | "JooYoung ",
82 | "Tobias Speicher ",
83 | "Steve Blaurock ",
84 | "Kyrylo Shegeda ",
85 | "Diwank Singh Tomer ",
86 | "Sören Balko ",
87 | "Arpit Solanki ",
88 | "Yuki Ito ",
89 | "Artur Zayats "
90 | ],
91 | "funding": {
92 | "type": "opencollective",
93 | "url": "https://opencollective.com/peer"
94 | },
95 | "collective": {
96 | "type": "opencollective",
97 | "url": "https://opencollective.com/peer"
98 | },
99 | "files": [
100 | "dist/*"
101 | ],
102 | "sideEffects": [
103 | "lib/global.ts",
104 | "lib/supports.ts"
105 | ],
106 | "main": "dist/bundler.cjs",
107 | "module": "dist/bundler.mjs",
108 | "browser-minified": "dist/peerjs.min.js",
109 | "browser-unminified": "dist/peerjs.js",
110 | "browser-minified-msgpack": "dist/serializer.msgpack.mjs",
111 | "types": "dist/types.d.ts",
112 | "engines": {
113 | "node": ">= 14"
114 | },
115 | "targets": {
116 | "types": {
117 | "source": "lib/exports.ts"
118 | },
119 | "main": {
120 | "source": "lib/exports.ts",
121 | "sourceMap": {
122 | "inlineSources": true
123 | }
124 | },
125 | "module": {
126 | "source": "lib/exports.ts",
127 | "includeNodeModules": [
128 | "eventemitter3"
129 | ],
130 | "sourceMap": {
131 | "inlineSources": true
132 | }
133 | },
134 | "browser-minified": {
135 | "context": "browser",
136 | "outputFormat": "global",
137 | "optimize": true,
138 | "engines": {
139 | "browsers": "chrome >= 83, edge >= 83, firefox >= 80, safari >= 15"
140 | },
141 | "source": "lib/global.ts"
142 | },
143 | "browser-unminified": {
144 | "context": "browser",
145 | "outputFormat": "global",
146 | "optimize": false,
147 | "engines": {
148 | "browsers": "chrome >= 83, edge >= 83, firefox >= 80, safari >= 15"
149 | },
150 | "source": "lib/global.ts"
151 | },
152 | "browser-minified-msgpack": {
153 | "context": "browser",
154 | "outputFormat": "esmodule",
155 | "isLibrary": true,
156 | "optimize": true,
157 | "engines": {
158 | "browsers": "chrome >= 83, edge >= 83, firefox >= 102, safari >= 15"
159 | },
160 | "source": "lib/dataconnection/StreamConnection/MsgPack.ts"
161 | }
162 | },
163 | "scripts": {
164 | "contributors": "git-authors-cli --print=false && prettier --write package.json && git add package.json package-lock.json && git commit -m \"chore(contributors): update and sort contributors list\"",
165 | "check": "tsc --noEmit && tsc -p e2e/tsconfig.json --noEmit",
166 | "watch": "parcel watch",
167 | "build": "rm -rf dist && parcel build",
168 | "prepublishOnly": "npm run build",
169 | "test": "jest",
170 | "test:watch": "jest --watch",
171 | "coverage": "jest --coverage --collectCoverageFrom=\"./lib/**\"",
172 | "format": "prettier --write .",
173 | "format:check": "prettier --check .",
174 | "semantic-release": "semantic-release",
175 | "e2e": "wdio run e2e/wdio.local.conf.ts",
176 | "e2e:bstack": "wdio run e2e/wdio.bstack.conf.ts"
177 | },
178 | "devDependencies": {
179 | "@parcel/config-default": "^2.9.3",
180 | "@parcel/packager-ts": "^2.9.3",
181 | "@parcel/transformer-typescript-tsc": "^2.9.3",
182 | "@parcel/transformer-typescript-types": "^2.9.3",
183 | "@semantic-release/changelog": "^6.0.1",
184 | "@semantic-release/git": "^10.0.1",
185 | "@swc/core": "^1.3.27",
186 | "@swc/jest": "^0.2.24",
187 | "@types/jasmine": "^5.0.0",
188 | "@wdio/browserstack-service": "^8.11.2",
189 | "@wdio/cli": "^8.11.2",
190 | "@wdio/globals": "^8.11.2",
191 | "@wdio/jasmine-framework": "^8.11.2",
192 | "@wdio/local-runner": "^8.11.2",
193 | "@wdio/spec-reporter": "^8.11.2",
194 | "@wdio/types": "^8.10.4",
195 | "http-server": "^14.1.1",
196 | "jest": "^29.3.1",
197 | "jest-environment-jsdom": "^29.3.1",
198 | "mock-socket": "^9.0.0",
199 | "parcel": "^2.9.3",
200 | "prettier": "^3.0.0",
201 | "semantic-release": "^23.0.0",
202 | "ts-node": "^10.9.1",
203 | "typescript": "^5.0.0",
204 | "wdio-geckodriver-service": "^5.0.1"
205 | },
206 | "dependencies": {
207 | "@msgpack/msgpack": "^2.8.0",
208 | "eventemitter3": "^4.0.7",
209 | "peerjs-js-binarypack": "^2.1.0",
210 | "webrtc-adapter": "^9.0.0"
211 | },
212 | "alias": {
213 | "process": false,
214 | "buffer": false
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:base", ":assignAndReview(jonasgloning)"],
4 | "labels": ["dependencies"],
5 | "assignees": ["jonasgloning"],
6 | "major": {
7 | "dependencyDashboardApproval": true
8 | },
9 | "packageRules": [
10 | {
11 | "matchDepTypes": ["devDependencies"],
12 | "addLabels": ["dev-dependencies"],
13 | "automerge": true,
14 | "automergeType": "branch"
15 | },
16 | {
17 | "matchUpdateTypes": ["minor", "patch"],
18 | "matchCurrentVersion": "!/^0/",
19 | "automerge": true,
20 | "automergeType": "branch"
21 | }
22 | ],
23 | "lockFileMaintenance": {
24 | "enabled": true,
25 | "automerge": true,
26 | "automergeType": "branch"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "downlevelIteration": true,
6 | "noUnusedLocals": true,
7 | "noUnusedParameters": true,
8 | "skipLibCheck": true,
9 | "isolatedModules": true,
10 | "resolveJsonModule": true,
11 | "lib": ["es2020", "dom"],
12 | "paths": {
13 | "cbor-x/index-no-eval": ["./node_modules/cbor-x"]
14 | }
15 | },
16 | "exclude": ["node_modules", "dist", "e2e"]
17 | }
18 |
--------------------------------------------------------------------------------