├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── build-linux.yml │ ├── build-mac-m1.yml │ ├── build-mac-x64.yml │ ├── build-win-arm64-test.yml │ ├── build-win.yml │ ├── lint.yml │ └── npm-publish-manual.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json └── tasks.json ├── API.md ├── BULDING.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build-containers ├── Dockerfile.alpine └── Dockerfile.debian ├── cmake └── toolchain │ └── ci.cmake ├── examples ├── build │ ├── Dockerfile │ └── README.md ├── client-server │ ├── README.md │ ├── client-benchmark.js │ ├── client-periodic.js │ ├── client.js │ ├── package-lock.json │ ├── package.json │ └── signaling-server.js ├── electron-demo │ ├── .gitignore │ ├── README.md │ ├── forge.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── index.html │ │ ├── main.js │ │ ├── node-datachannel.js │ │ ├── preload.js │ │ └── renderer.js │ ├── webpack.main.config.js │ ├── webpack.renderer.config.js │ └── webpack.rules.js ├── media │ ├── README.md │ ├── main.html │ ├── media.js │ ├── package-lock.json │ └── package.json ├── simple-peer-usage │ ├── main.js │ ├── package-lock.json │ └── package.json ├── use-cases │ ├── commonjs │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ ├── esm │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── typescript │ │ ├── index.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ └── tsconfig.json └── websocket │ ├── package-lock.json │ ├── package.json │ ├── websocket-client.js │ └── websocket-server.js ├── jest.config.ts ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── cpp │ ├── data-channel-wrapper.cpp │ ├── data-channel-wrapper.h │ ├── ice-udp-mux-listener-wrapper.cpp │ ├── ice-udp-mux-listener-wrapper.h │ ├── main.cpp │ ├── media-audio-wrapper.cpp │ ├── media-audio-wrapper.h │ ├── media-direction.cpp │ ├── media-direction.h │ ├── media-rtcpreceivingsession-wrapper.cpp │ ├── media-rtcpreceivingsession-wrapper.h │ ├── media-track-wrapper.cpp │ ├── media-track-wrapper.h │ ├── media-video-wrapper.cpp │ ├── media-video-wrapper.h │ ├── peer-connection-wrapper.cpp │ ├── peer-connection-wrapper.h │ ├── rtc-wrapper.cpp │ ├── rtc-wrapper.h │ ├── thread-safe-callback.cpp │ ├── thread-safe-callback.h │ ├── web-socket-server-wrapper.cpp │ ├── web-socket-server-wrapper.h │ ├── web-socket-wrapper.cpp │ └── web-socket-wrapper.h ├── index.ts ├── lib │ ├── datachannel-stream.ts │ ├── index.ts │ ├── node-datachannel.ts │ ├── types.ts │ ├── websocket-server.ts │ └── websocket.ts └── polyfill │ ├── Events.ts │ ├── Exception.ts │ ├── README.md │ ├── RTCCertificate.ts │ ├── RTCDataChannel.ts │ ├── RTCDtlsTransport.ts │ ├── RTCError.ts │ ├── RTCIceCandidate.ts │ ├── RTCIceTransport.ts │ ├── RTCPeerConnection.ts │ ├── RTCSctpTransport.ts │ ├── RTCSessionDescription.ts │ └── index.ts ├── test ├── connectivity.ts ├── fixtures │ └── event-promise.ts ├── jest-tests │ ├── basic.test.ts │ ├── multiple-run.test.ts │ ├── p2p.test.ts │ ├── polyfill.test.ts │ ├── streams.test.ts │ └── websocket.test.ts ├── leak-test │ ├── index.js │ ├── package-lock.json │ └── package.json ├── polyfill-connectivity.ts ├── websockets.ts └── wpt-tests │ ├── README.md │ ├── chrome-failed-tests.ts │ ├── chromeFailedTests.json │ ├── index.ts │ ├── last-test-results.md │ ├── wpt-test-list.ts │ └── wpt.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | /**/*.js 5 | jest.config.ts 6 | rollup.config.mjs 7 | test/wpt-tests/ 8 | examples 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "tsconfig.json", 10 | "sourceType": "module", 11 | "ecmaVersion": 2020 12 | }, 13 | "plugins": ["@typescript-eslint", "jest"], 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:jest/recommended", 18 | "prettier" 19 | ], 20 | "rules": { 21 | // The following rule is enabled only to supplement the inline suppression 22 | // examples, and because it is not a recommended rule, you should either 23 | // disable it, or understand what it enforces. 24 | // https://typescript-eslint.io/rules/explicit-function-return-type/ 25 | "@typescript-eslint/explicit-function-return-type": "warn" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | open_collective: node-datachannel 3 | -------------------------------------------------------------------------------- /.github/workflows/build-linux.yml: -------------------------------------------------------------------------------- 1 | name: Build - Linux 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # tags: 7 | # - v* 8 | 9 | env: 10 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 11 | 12 | jobs: 13 | build-linux: 14 | runs-on: ubuntu-22.04 15 | strategy: 16 | matrix: 17 | node-version: [18] 18 | arch: [amd64, arm64, arm] 19 | platform: [debian, alpine] 20 | include: 21 | # Platform-specific baselines 22 | - platform: alpine 23 | triple: alpine-linux-musl 24 | version: 3.16 25 | libc: musl 26 | gcc_install: clang lld 27 | gcc: clang 28 | gxx: clang++ 29 | - platform: debian 30 | triple: linux-gnu 31 | version: bullseye 32 | # libc intentionally not set (prebuild requirement) 33 | 34 | # Arch-specific baselines 35 | - arch: amd64 36 | triple_arch: x86_64 37 | node_arch: x64 38 | - arch: arm64 39 | triple_arch: aarch64 40 | node_arch: arm64 41 | flags: -march=armv8-a -fPIC 42 | - arch: arm 43 | triple_arch: armv7 44 | node_arch: arm 45 | triple_postfix: eabihf 46 | flags: -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -fPIC 47 | 48 | # Platform + Arch overrides (needed for proper toolchain) 49 | - arch: amd64 50 | platform: debian 51 | gcc_install: gcc g++ 52 | gcc: gcc 53 | gxx: g++ 54 | - arch: arm64 55 | platform: debian 56 | gcc_install: gcc-aarch64-linux-gnu g++-aarch64-linux-gnu 57 | gcc: aarch64-linux-gnu-gcc 58 | gxx: aarch64-linux-gnu-g++ 59 | - arch: arm 60 | platform: debian 61 | triple_arch: arm 62 | gcc_install: gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf 63 | gcc: arm-linux-gnueabihf-gcc 64 | gxx: arm-linux-gnueabihf-g++ 65 | 66 | steps: 67 | - uses: actions/checkout@v4 68 | 69 | - name: Use Node.js ${{ matrix.node-version }} 70 | uses: actions/setup-node@v4 71 | with: 72 | node-version: ${{ matrix.node-version }} 73 | 74 | - name: Install node dependencies 75 | run: npm install --ignore-scripts 76 | env: 77 | CI: true 78 | 79 | - if: matrix.arch != 'amd64' 80 | name: Set up QEMU 81 | uses: docker/setup-qemu-action@v3 82 | with: 83 | platforms: ${{ matrix.arch }} 84 | 85 | - name: Install system dependencies 86 | run: | 87 | sudo apt update 88 | sudo apt install -y podman 89 | 90 | - name: Prepare build container 91 | run: > 92 | sudo podman build 93 | --isolation=chroot 94 | -t ndc-buildroot:${{ matrix.platform }}-${{ matrix.arch }} 95 | ./build-containers 96 | --file Dockerfile.${{ matrix.platform }} 97 | --platform linux/${{ matrix.arch }} 98 | --build-arg version=${{ matrix.version }} 99 | --build-arg node_version=${{ matrix.node-version }} 100 | --build-arg gcc_install="${{ matrix.gcc_install }}" 101 | 102 | - name: Build 103 | run: | 104 | sudo podman run --rm \ 105 | -e TRIPLE=${{ matrix.triple_arch }}-${{ matrix.triple }}${{ matrix.triple_postfix }} \ 106 | -e COMPILER_FLAGS="${{ matrix.flags }}" \ 107 | -e LIBC=${{ matrix.libc }} \ 108 | -e GCC=${{ matrix.gcc }} \ 109 | -e GXX=${{ matrix.gxx }} \ 110 | -e CI=true \ 111 | -v ${{ github.workspace }}:/usr/app \ 112 | --platform linux/${{ matrix.arch }} \ 113 | -w /usr/app \ 114 | ndc-buildroot:${{ matrix.platform }}-${{ matrix.arch }} \ 115 | node_modules/.bin/prebuild --arch ${{ matrix.node_arch }} -r napi --backend cmake-js -- --CDCMAKE_TOOLCHAIN_FILE:FILEPATH=./cmake/toolchain/ci.cmake 116 | 117 | - name: Test 118 | run: > 119 | sudo podman run --rm 120 | -v ${{ github.workspace }}:/usr/app/ 121 | -e CI=true 122 | --platform linux/${{ matrix.arch }} 123 | -t ndc-buildroot:${{ matrix.platform }}-${{ matrix.arch }} 124 | npm run test 125 | 126 | - name: Upload 127 | run: sudo --preserve-env=CI,LIBC node_modules/.bin/prebuild --arch ${{ matrix.node_arch }} -r napi --upload -u ${{ secrets.GITHUB_TOKEN }} 128 | env: 129 | LIBC: ${{ matrix.libc }} 130 | CI: true 131 | -------------------------------------------------------------------------------- /.github/workflows/build-mac-m1.yml: -------------------------------------------------------------------------------- 1 | name: Build - Mac M1 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # tags: 7 | # - v* 8 | 9 | env: 10 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 11 | 12 | jobs: 13 | build-macos: 14 | runs-on: macos-13 15 | strategy: 16 | matrix: 17 | node-version: [18] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install OpenSSL 22 | run: | 23 | wget https://mac.r-project.org/bin/darwin20/arm64/openssl-1.1.1t-darwin.20-arm64.tar.xz 24 | tar -xf openssl-1.1.1t-darwin.20-arm64.tar.xz -C /tmp 25 | rm -rf openssl-1.1.1t-darwin.20-arm64.tar.xz 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | - name: Build 31 | run: | 32 | npm install --ignore-scripts 33 | node_modules/.bin/prebuild -r napi --backend cmake-js --arch arm64 --upload -u ${{ secrets.GITHUB_TOKEN }} -- --CDCMAKE_OSX_ARCHITECTURES="arm64" 34 | env: 35 | CI: true 36 | OPENSSL_ROOT_DIR: /tmp/opt/R/arm64 37 | OPENSSL_LIBRARIES: /tmp/opt/R/arm64/lib 38 | -------------------------------------------------------------------------------- /.github/workflows/build-mac-x64.yml: -------------------------------------------------------------------------------- 1 | name: Build - Mac x64 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # tags: 7 | # - v* 8 | 9 | env: 10 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 11 | 12 | jobs: 13 | build-macos: 14 | runs-on: macos-13 15 | strategy: 16 | matrix: 17 | node-version: [18] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install OpenSSL 22 | run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew reinstall openssl@3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: Build 28 | run: npm install --build-from-source 29 | env: 30 | CI: true 31 | OPENSSL_ROOT_DIR: /usr/local/opt/openssl@3 32 | OPENSSL_LIBRARIES: /usr/local/opt/openssl@3/lib 33 | - name: Test 34 | run: npm run test 35 | env: 36 | CI: true 37 | - name: Upload 38 | run: node_modules/.bin/prebuild -r napi --upload -u ${{ secrets.GITHUB_TOKEN }} 39 | env: 40 | CI: true 41 | -------------------------------------------------------------------------------- /.github/workflows/build-win-arm64-test.yml: -------------------------------------------------------------------------------- 1 | name: Build - Win - Arm64 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # tags: 7 | # - v* 8 | 9 | env: 10 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 11 | 12 | jobs: 13 | build-windows-arm64: 14 | runs-on: windows-2022 15 | strategy: 16 | matrix: 17 | node-version: [18] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Set Up MSVC for ARM64 Cross Compilation 28 | uses: ilammy/msvc-dev-cmd@v1 29 | with: 30 | arch: amd64_arm64 # Sets up cross-compilation from x64 to ARM64 31 | 32 | - name: Install vcpkg 33 | run: | 34 | git clone https://github.com/microsoft/vcpkg.git C:\vcpkg 35 | cd C:\vcpkg 36 | .\bootstrap-vcpkg.bat 37 | shell: cmd 38 | 39 | - name: Install OpenSSL (Static) for Windows ARM64 40 | run: | 41 | C:\vcpkg\vcpkg install openssl:arm64-windows-static 42 | shell: cmd 43 | 44 | - name: Build 45 | run: | 46 | npm install --ignore-scripts 47 | node_modules/.bin/prebuild -r napi --backend cmake-js --arch arm64 --upload -u ${{ secrets.GITHUB_TOKEN }} -- --CDCMAKE_TOOLCHAIN_FILE=c:\vcpkg\scripts\buildsystems\vcpkg.cmake --CDVCPKG_TARGET_TRIPLET=arm64-windows-static 48 | 49 | - name: Upload 50 | run: node_modules/.bin/prebuild -r napi --upload -u ${{ secrets.GITHUB_TOKEN }} 51 | env: 52 | CI: true 53 | -------------------------------------------------------------------------------- /.github/workflows/build-win.yml: -------------------------------------------------------------------------------- 1 | name: Build - Win 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # tags: 7 | # - v* 8 | 9 | env: 10 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 11 | 12 | jobs: 13 | build-windows-x64: 14 | runs-on: windows-2022 15 | strategy: 16 | matrix: 17 | node-version: [18] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install OpenSSL 22 | run: choco install openssl -y 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: Build 28 | run: npm install --build-from-source 29 | env: 30 | CI: true 31 | # - name: Test 32 | # run: npm run test 33 | # env: 34 | # CI: true 35 | - name: Upload 36 | run: node_modules/.bin/prebuild -r napi --upload -u ${{ secrets.GITHUB_TOKEN }} 37 | env: 38 | CI: true 39 | 40 | build-windows-x86: 41 | runs-on: windows-2019 42 | strategy: 43 | matrix: 44 | node_version: [18] 45 | node_arch: 46 | - x86 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: ilammy/msvc-dev-cmd@v1 50 | with: 51 | arch: x86 52 | - name: Install OpenSSL 53 | # openssl v3.1.1 [Approved] 54 | # openssl package files install completed. Performing other installation steps. 55 | # ERROR: 32-bit installation is not supported for openssl 56 | run: choco install openssl --x86=true --version=1.1.1.2100 57 | - name: Use Node.js ${{ matrix.node_version }} 58 | uses: aminya/setup-node@x86 59 | with: 60 | node-version: ${{ matrix.node_version }} 61 | node-arch: ${{ matrix.node_arch }} 62 | - name: Build 63 | run: npm install --build-from-source 64 | env: 65 | CI: true 66 | # - name: Test 67 | # run: npm run test 68 | # env: 69 | # CI: true 70 | - name: Upload 71 | run: node_modules/.bin/prebuild -r napi --upload -u ${{ secrets.GITHUB_TOKEN }} 72 | env: 73 | CI: true 74 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 75 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Use Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: '18' 17 | - run: npm ci 18 | - run: npm run lint 19 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-manual.yml: -------------------------------------------------------------------------------- 1 | name: npm Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | npm-publish: 8 | name: npm-publish 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 20.x 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Publish 19 | run: | 20 | npm install --ignore-scripts 21 | npm publish 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | prebuilds 4 | node_modules 5 | .vscode/* 6 | !.vscode/tasks.json 7 | !.vscode/launch.json 8 | tmp 9 | coverage 10 | vcpkg_installed 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/wpt-tests/wpt"] 2 | path = test/wpt-tests/wpt 3 | url = https://github.com/web-platform-tests/wpt.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build 2 | prebuilds 3 | node_modules 4 | .vscode 5 | tmp 6 | coverage 7 | test 8 | .github 9 | build-containers 10 | examples 11 | cmake -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/wpt-tests/wpt 2 | vcpkg 3 | out 4 | .webpack 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.mts"], 8 | "options": { 9 | "parser": "typescript" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // On first run clean build folder. 6 | // Sometimes it has still Release config 7 | "type": "lldb", 8 | "request": "launch", 9 | "name": "test-app", 10 | "preLaunchTask": "npm: build:debug", 11 | "program": "/home/murat/.nvm/versions/node/v20.11.1/bin/node", 12 | "cwd": "${workspaceFolder}/test/leak-test", 13 | "args": ["${workspaceFolder}/test/leak-test/index.js"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build:debug", 7 | "group": "build", 8 | "problemMatcher": [], 9 | "label": "npm: build:debug", 10 | "detail": "npm run build:debug" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## PeerConnection Class 4 | 5 | **Constructor** 6 | 7 | let pc = new PeerConnection(peerName[,options]) 8 | 9 | - peerName `` Peer name to use for logs etc.. 10 | - options `` WebRTC Config Options 11 | 12 | ``` 13 | export interface RtcConfig { 14 | iceServers: (string | IceServer)[]; 15 | proxyServer?: ProxyServer; 16 | bindAddress?: string; 17 | enableIceTcp?: boolean; 18 | enableIceUdpMux?: boolean; 19 | disableAutoNegotiation?: boolean; 20 | disableFingerprintVerification?: boolean; 21 | disableAutoGathering?: boolean; 22 | forceMediaTransport?: boolean; 23 | portRangeBegin?: number; 24 | portRangeEnd?: number; 25 | maxMessageSize?: number; 26 | mtu?: number; 27 | iceTransportPolicy?: TransportPolicy; 28 | disableFingerprintVerification?: boolean; 29 | certificatePemFile?: string; 30 | keyPemFile?: string; 31 | keyPemPass?: string; 32 | } 33 | 34 | export const enum RelayType { 35 | TurnUdp = 'TurnUdp', 36 | TurnTcp = 'TurnTcp', 37 | TurnTls = 'TurnTls' 38 | } 39 | 40 | export interface IceServer { 41 | hostname: string; 42 | port: number; 43 | username?: string; 44 | password?: string; 45 | relayType?: RelayType; 46 | } 47 | 48 | export type TransportPolicy = 'all' | 'relay'; 49 | 50 | "iceServers" option is an array of stun/turn server urls 51 | Examples; 52 | STUN Server Example : stun:stun.l.google.com:19302 53 | TURN Server Example : turn:USERNAME:PASSWORD@TURN_IP_OR_ADDRESS:PORT 54 | TURN Server Example (TCP) : turn:USERNAME:PASSWORD@TURN_IP_OR_ADDRESS:PORT?transport=tcp 55 | TURN Server Example (TLS) : turns:USERNAME:PASSWORD@TURN_IP_OR_ADDRESS:PORT 56 | 57 | ``` 58 | 59 | **close: () => void** 60 | 61 | Close Peer Connection 62 | 63 | **destroy: () => void** 64 | 65 | Close Peer Connection & Clear all callbacks 66 | 67 | **setRemoteDescription: (sdp: string, type: DescriptionType) => void** 68 | 69 | Set Remote Description 70 | 71 | ``` 72 | export const enum DescriptionType { 73 | Unspec = 'Unspec', 74 | Offer = 'Offer', 75 | Answer = 'Answer' 76 | } 77 | ``` 78 | 79 | **setLocalDescription: (sdp: string, init?: LocalDescriptionInit) => void** 80 | 81 | Set Local Description and optionally the ICE ufrag/pwd to use. These should not 82 | be set as they will be generated automatically as per the spec. 83 | ``` 84 | export interface LocalDescriptionInit { 85 | iceUfrag?: string; 86 | icePwd?: string; 87 | } 88 | ``` 89 | 90 | **remoteFingerprint: () => CertificateFingerprint** 91 | 92 | Returns the certificate fingerprint used by the remote peer 93 | ``` 94 | export interface CertificateFingerprint { 95 | value: string; 96 | algorithm: 'sha-1' | 'sha-224' | 'sha-256' | 'sha-384' | 'sha-512' | 'md5' | 'md2'; 97 | } 98 | ``` 99 | 100 | **addRemoteCandidate: (candidate: string, mid: string) => void** 101 | 102 | Add remote candidate info 103 | 104 | **createDataChannel: (label: string, config?: DataChannelInitConfig) => DataChannel** 105 | 106 | Create new data-channel 107 | 108 | - label `` Data channel name 109 | - config `` Data channel options 110 | 111 | ``` 112 | export interface DataChannelInitConfig { 113 | protocol?: string; 114 | negotiated?: boolean; 115 | id?: number; 116 | unordered?: boolean; // Reliability 117 | maxPacketLifeTime?: number; // Reliability 118 | maxRetransmits?: number; // Reliability 119 | } 120 | ``` 121 | 122 | **state: () => string** 123 | 124 | Get current state 125 | 126 | **signalingState: () => string** 127 | 128 | Get current signaling state 129 | 130 | **gatheringState: () => string** 131 | 132 | Get current gathering state 133 | 134 | **onLocalDescription: (cb: (sdp: string, type: DescriptionType) => void) => void** 135 | 136 | Local Description Callback 137 | 138 | ``` 139 | export const enum DescriptionType { 140 | Unspec = 'Unspec', 141 | Offer = 'Offer', 142 | Answer = 'Answer' 143 | } 144 | ``` 145 | 146 | **onLocalCandidate: (cb: (candidate: string, mid: string) => void) => void** 147 | 148 | Local Candidate Callback 149 | 150 | **onStateChange: (cb: (state: string) => void) => void** 151 | 152 | State Change Callback 153 | 154 | **onSignalingStateChange: (state: (sdp: string) => void) => void** 155 | 156 | Signaling State Change Callback 157 | 158 | **onGatheringStateChange: (state: (sdp: string) => void) => void** 159 | 160 | Gathering State Change Callback 161 | 162 | **onDataChannel: (cb: (dc: DataChannel) => void) => void** 163 | 164 | New Data Channel Callback 165 | 166 | **bytesSent: () => number** 167 | 168 | Get bytes sent stat 169 | 170 | **bytesReceived: () => number** 171 | 172 | Get bytes received stat 173 | 174 | **rtt: () => number** 175 | 176 | Get rtt stat 177 | 178 | **getSelectedCandidatePair: () => { local: SelectedCandidateInfo, remote: SelectedCandidateInfo }** 179 | 180 | Get info about selected candidate pair 181 | 182 | ``` 183 | export interface SelectedCandidateInfo { 184 | address: string; 185 | port: number; 186 | type: string; 187 | transportType: string; 188 | } 189 | ``` 190 | 191 | ## DataChannel Class 192 | 193 | > You can create a new Datachannel instance by calling `PeerConnection.createDataChannel` function. 194 | 195 | **close: () => void** 196 | 197 | Close data channel 198 | 199 | **getLabel: () => string** 200 | 201 | Get label of data-channel 202 | 203 | **sendMessage: (msg: string) => boolean** 204 | 205 | Send Message as string 206 | 207 | **sendMessageBinary: (buffer: Buffer) => boolean** 208 | 209 | Send Message as binary 210 | 211 | **isOpen: () => boolean** 212 | 213 | Query data-channel 214 | 215 | **bufferedAmount: () => number** 216 | 217 | Get current buffered amount level 218 | 219 | **maxMessageSize: () => number** 220 | 221 | Get max message size of the data-channel, that could be sent 222 | 223 | **setBufferedAmountLowThreshold: (newSize: number) => void** 224 | 225 | Set buffer level of the `onBufferedAmountLow` callback 226 | 227 | **onOpen: (cb: () => void) => void** 228 | 229 | Open callback 230 | 231 | **onClosed: (cb: () => void) => void** 232 | 233 | Closed callback 234 | 235 | **onError: (cb: (err: string) => void) => void** 236 | 237 | Error callback 238 | 239 | **onBufferedAmountLow: (cb: () => void) => void** 240 | 241 | Buffer level low callback 242 | 243 | **onMessage: (cb: (msg: string | Buffer) => void) => void** 244 | 245 | New Message callback 246 | -------------------------------------------------------------------------------- /BULDING.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | ## Requirements 4 | 5 | - cmake >= V3.14 6 | - [libdatachannel dependencies](https://github.com/paullouisageneau/libdatachannel/blob/master/README.md#dependencies) 7 | 8 | ## Building from source 9 | 10 | ```sh 11 | > git clone https://github.com/murat-dogan/node-datachannel.git 12 | > cd node-datachannel 13 | > npm i 14 | ``` 15 | 16 | Other Options 17 | 18 | ```sh 19 | # Use GnuTLS instead of OpenSSL (Default False) 20 | > npm run install-gnu-tls 21 | 22 | # Use libnice instead of libjuice (Default False) 23 | # libnice-dev packet should be installed. (eg. sudo apt install libnice-dev) 24 | > npm run install-nice 25 | ``` 26 | 27 | Compile without Media and Websocket 28 | ```sh 29 | npx cmake-js clean 30 | npx cmake-js configure --CDNO_MEDIA=ON --CDNO_WEBSOCKET=ON 31 | npx cmake-js build 32 | ``` 33 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | cmake_policy(SET CMP0091 NEW) 3 | cmake_policy(SET CMP0042 NEW) 4 | 5 | project(node_datachannel VERSION 0.28.0) 6 | 7 | # -Dnapi_build_version=8 8 | add_definitions(-DNAPI_VERSION=8) 9 | 10 | include_directories(${CMAKE_JS_INC}) 11 | 12 | if(WIN32) 13 | set(OPENSSL_MSVC_STATIC_RT TRUE) 14 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 15 | endif() 16 | 17 | if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Android") 18 | set(CMAKE_C_FLAGS "-Wno-error=unused-but-set-variable -Wno-error=strict-prototypes") 19 | endif() 20 | 21 | # Add -frtti only for Linux and macOS 22 | if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") 23 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -frtti") 24 | endif() 25 | 26 | set(OPENSSL_USE_STATIC_LIBS TRUE) 27 | find_package(OpenSSL REQUIRED) 28 | 29 | include(FetchContent) 30 | 31 | # Fetch libdatachannel 32 | FetchContent_Declare( 33 | libdatachannel 34 | GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git 35 | GIT_TAG "v0.23.0" 36 | ) 37 | 38 | option(NO_MEDIA "Disable media transport support in libdatachannel" OFF) 39 | option(NO_WEBSOCKET "Disable WebSocket support in libdatachannel" OFF) 40 | 41 | set(FETCHCONTENT_QUIET OFF) 42 | FetchContent_GetProperties(libdatachannel) 43 | 44 | if(NOT libdatachannel) 45 | FetchContent_Populate(libdatachannel) 46 | add_subdirectory(${libdatachannel_SOURCE_DIR} ${libdatachannel_BINARY_DIR} EXCLUDE_FROM_ALL) 47 | endif() 48 | 49 | # Create Source File List 50 | set(SRC_FILES 51 | src/cpp/rtc-wrapper.cpp 52 | src/cpp/data-channel-wrapper.cpp 53 | src/cpp/ice-udp-mux-listener-wrapper.cpp 54 | src/cpp/peer-connection-wrapper.cpp 55 | src/cpp/thread-safe-callback.cpp 56 | src/cpp/main.cpp 57 | ${CMAKE_JS_SRC} 58 | ) 59 | 60 | if(NOT NO_MEDIA) 61 | list(APPEND SRC_FILES 62 | src/cpp/media-direction.cpp 63 | src/cpp/media-rtcpreceivingsession-wrapper.cpp 64 | src/cpp/media-track-wrapper.cpp 65 | src/cpp/media-audio-wrapper.cpp 66 | src/cpp/media-video-wrapper.cpp 67 | ) 68 | endif() 69 | 70 | if(NOT NO_WEBSOCKET) 71 | list(APPEND SRC_FILES 72 | src/cpp/web-socket-wrapper.cpp 73 | src/cpp/web-socket-server-wrapper.cpp 74 | ) 75 | endif() 76 | 77 | add_library(${PROJECT_NAME} SHARED ${SRC_FILES}) 78 | 79 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17) 80 | set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") 81 | 82 | # Set the output directory for all build types 83 | # set_target_properties(${PROJECT_NAME} PROPERTIES 84 | # RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/binary 85 | # LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/binary 86 | # ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/binary 87 | # ) 88 | 89 | target_include_directories(${PROJECT_NAME} PRIVATE 90 | ${CMAKE_SOURCE_DIR}/node_modules/node-addon-api 91 | ${CMAKE_BINARY_DIR}/_deps/libdatachannel-src/include 92 | ${CMAKE_BINARY_DIR}/_deps/libdatachannel-src/deps/plog 93 | ) 94 | 95 | set(LINK_LIBRARIES 96 | ${CMAKE_JS_LIB} 97 | datachannel-static 98 | plog::plog 99 | ) 100 | 101 | if(APPLE) 102 | # 103 | elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Android") 104 | list(APPEND LINK_LIBRARIES -static-libgcc) 105 | elseif(UNIX) 106 | list(APPEND LINK_LIBRARIES -static-libgcc -static-libstdc++) 107 | endif() 108 | 109 | if(WIN32) 110 | SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT") 111 | list(APPEND LINK_LIBRARIES crypt32.lib) 112 | endif() 113 | 114 | target_link_libraries(${PROJECT_NAME} PRIVATE ${LINK_LIBRARIES}) 115 | 116 | if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) 117 | # Generate node.lib 118 | execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) 119 | endif() 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTC For Node.js and Electron ( with WebSocket) 2 | 3 | ![Linux CI Build](https://github.com/murat-dogan/node-datachannel/workflows/Build%20-%20Linux/badge.svg) ![Windows CI Build](https://github.com/murat-dogan/node-datachannel/workflows/Build%20-%20Win/badge.svg) ![Mac x64 CI Build](https://github.com/murat-dogan/node-datachannel/workflows/Build%20-%20Mac%20x64/badge.svg) ![Mac M1 CI Build](https://github.com/murat-dogan/node-datachannel/workflows/Build%20-%20Mac%20M1/badge.svg) 4 | 5 | - Lightweight 6 | - No need to deal with WebRTC stack! 7 | - Small binary sizes (~8MB for Linux x64) 8 | - Type infos for Typescript 9 | - Integrated WebSocket Client & Server Implementation 10 | 11 | This project is Node.js bindings for [libdatachannel](https://github.com/paullouisageneau/libdatachannel) library. 12 | 13 | ## Install 14 | 15 | ```sh 16 | npm install node-datachannel 17 | ``` 18 | 19 | ## Supported Platforms 20 | 21 | `node-datachannel` targets N-API version 8 and supports Node.js v18.20 and above. It is tested on Linux, Windows and MacOS. For N-API compatibility please check [here](https://nodejs.org/api/n-api.html#n_api_n_api_version_matrix). 22 | 23 | | | Linux [x64,armv7,arm64] (1) | Windows [x86,x64] | Mac [M1,x64] | 24 | | ------------------------- | :-------------------------: | :---------------: | :----------: | 25 | | N-API v8 (>= Node.js v18) | + | + | + | 26 | 27 | **(1)** For Linux musl + libc 28 | 29 | ## Electron 30 | 31 | `node-datachannel` supports Electron. 32 | 33 | Please check [electron demo](/examples/electron-demo) 34 | 35 | ## WebRTC Polyfills 36 | 37 | WebRTC polyfills to be used for libraries like `simple-peer`. 38 | 39 | Please check [here for more](/src/polyfill) 40 | 41 | ### web-platform-tests 42 | 43 | Please check actual situation [here](/test/wpt-tests/) 44 | 45 | ## WebSocket Client & Server 46 | 47 | Integrated WebSocket Client & Server is available, which can be used separately or for signaling. 48 | 49 | For an example usage, [check here](/examples/websocket) 50 | 51 | ## Example Usage 52 | 53 | ```js 54 | import nodeDataChannel from 'node-datachannel'; 55 | 56 | // Log Level 57 | nodeDataChannel.initLogger('Debug'); 58 | 59 | // Integrated WebSocket available and can be used for signaling etc 60 | // const ws = new nodeDataChannel.WebSocket(); 61 | 62 | let dc1 = null; 63 | let dc2 = null; 64 | 65 | let peer1 = new nodeDataChannel.PeerConnection('Peer1', { 66 | iceServers: ['stun:stun.l.google.com:19302'], 67 | }); 68 | 69 | peer1.onLocalDescription((sdp, type) => { 70 | peer2.setRemoteDescription(sdp, type); 71 | }); 72 | peer1.onLocalCandidate((candidate, mid) => { 73 | peer2.addRemoteCandidate(candidate, mid); 74 | }); 75 | 76 | let peer2 = new nodeDataChannel.PeerConnection('Peer2', { 77 | iceServers: ['stun:stun.l.google.com:19302'], 78 | }); 79 | 80 | peer2.onLocalDescription((sdp, type) => { 81 | peer1.setRemoteDescription(sdp, type); 82 | }); 83 | peer2.onLocalCandidate((candidate, mid) => { 84 | peer1.addRemoteCandidate(candidate, mid); 85 | }); 86 | peer2.onDataChannel((dc) => { 87 | dc2 = dc; 88 | dc2.onMessage((msg) => { 89 | console.log('Peer2 Received Msg:', msg); 90 | }); 91 | dc2.sendMessage('Hello From Peer2'); 92 | }); 93 | 94 | dc1 = peer1.createDataChannel('test'); 95 | 96 | dc1.onOpen(() => { 97 | dc1.sendMessage('Hello from Peer1'); 98 | }); 99 | 100 | dc1.onMessage((msg) => { 101 | console.log('Peer1 Received Msg:', msg); 102 | }); 103 | ``` 104 | 105 | ## Examples 106 | 107 | Please check [examples](/examples/) folder 108 | 109 | ## Test 110 | 111 | ```sh 112 | npm run test # Unit tests 113 | node test/connectivity.js # Connectivity 114 | ``` 115 | 116 | ## Build 117 | 118 | Please check [here](/BULDING.md) 119 | 120 | ## API Docs 121 | 122 | Please check [docs](/API.md) page 123 | 124 | ## Contributing 125 | 126 | Contributions are welcome! 127 | 128 | ## Thanks 129 | 130 | Thanks to [Streamr](https://streamr.network/) for supporting this project by being a Sponsor! 131 | -------------------------------------------------------------------------------- /build-containers/Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | ARG version=3.16 2 | ARG node_version=18 3 | 4 | # Accept build args 5 | ARG gcc_install 6 | 7 | FROM node:${node_version}-alpine${version} 8 | 9 | RUN apk add \ 10 | build-base \ 11 | git \ 12 | libc-dev \ 13 | python3 \ 14 | make \ 15 | musl-dev \ 16 | openssl-libs-static \ 17 | openssl-dev \ 18 | cmake \ 19 | ninja \ 20 | clang \ 21 | lld \ 22 | ${gcc_install} 23 | 24 | WORKDIR /usr/app/ 25 | -------------------------------------------------------------------------------- /build-containers/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | ARG version=bullseye 2 | ARG node_version=18 3 | 4 | # Accept build args 5 | ARG gcc_install 6 | 7 | FROM node:${node_version}-${version} 8 | 9 | RUN apt update 10 | RUN apt install -y --no-install-recommends \ 11 | build-essential \ 12 | libssl-dev \ 13 | cmake \ 14 | ninja-build \ 15 | ${gcc_install} 16 | 17 | WORKDIR /usr/app/ 18 | -------------------------------------------------------------------------------- /cmake/toolchain/ci.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(triple $ENV{TRIPLE}) 3 | 4 | # use clang and lld 5 | set(CMAKE_C_COMPILER $ENV{GCC}) 6 | set(CMAKE_CXX_COMPILER $ENV{GXX}) 7 | if (CMAKE_C_COMPILER MATCHES clang) 8 | add_link_options("-fuse-ld=lld") 9 | endif() 10 | 11 | set(CMAKE_SYSROOT "$ENV{SYSROOT}") 12 | message(STATUS "Using sysroot: ${CMAKE_SYSROOT}") 13 | 14 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 15 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 16 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 17 | 18 | set(CMAKE_C_COMPILER_TARGET ${triple}) 19 | set(CMAKE_CXX_COMPILER_TARGET ${triple}) 20 | message(STATUS "Compiling for: ${triple}") 21 | 22 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} $ENV{COMPILER_FLAGS}") 23 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} $ENV{COMPILER_FLAGS}") 24 | -------------------------------------------------------------------------------- /examples/build/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION=20.04 2 | 3 | FROM ubuntu:${UBUNTU_VERSION} 4 | 5 | RUN apt-get update -y && apt-get upgrade -y 6 | 7 | # Node 8 | # https://github.com/nodesource/distributions#nodejs 9 | ARG NODE_MAJOR=18 10 | RUN apt-get install -y ca-certificates curl gnupg 11 | RUN mkdir -p /etc/apt/keyrings 12 | RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg 13 | RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list 14 | RUN apt-get update -y 15 | RUN apt-get install -y nodejs 16 | 17 | # C++ 18 | RUN apt-get install -y cmake g++ 19 | 20 | # libnice library as an alternative to default one 21 | # RUN apt-get install -y libnice-dev 22 | 23 | # libssl-dev 24 | # https://www.claudiokuenzler.com/blog/1216/could-not-find-openssl-error-compiling-cmake-source 25 | RUN apt-get install -y libssl-dev 26 | 27 | # Git 28 | RUN apt-get install -y git 29 | 30 | WORKDIR /home/node-datachannel 31 | -------------------------------------------------------------------------------- /examples/build/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Building with Docker 4 | 5 | `cd` into `node-datachannel` root directory 6 | 7 | ```sh 8 | # Build Docker image from Dockerfile 9 | # You can choose Ubuntu and Node major versions by changing UBUNTU_VERSION and NODE_MAJOR 10 | > docker build \ 11 | -f ./examples/build/Dockerfile \ 12 | -t node-datachannel-dev \ 13 | --build-arg UBUNTU_VERSION=20.04 \ 14 | --build-arg NODE_MAJOR=18 \ 15 | . 16 | 17 | # Run Docker container and mount node-datachannel root directory as a volume to the container 18 | > docker run -it --rm --name node-datachannel-dev-container -v ${PWD}:/home/node-datachannel node-datachannel-dev /bin/bash 19 | 20 | # Run inside Docker container: 21 | > npm install --build-from-source 22 | ``` 23 | 24 | The build will be written to `prebuilds/` 25 | -------------------------------------------------------------------------------- /examples/client-server/README.md: -------------------------------------------------------------------------------- 1 | # client-server Example 2 | 3 | - You can use client-server example project to test WebRTC Data Channels with WebSocket signaling. 4 | - It uses same logic of [libdatachannel/examples/client](https://github.com/paullouisageneau/libdatachannel/tree/master/examples) project. 5 | - Contains an equivalent implementation for a node.js signaling server 6 | 7 | ## How to Use? 8 | 9 | - Prepare Project 10 | - cd examples/client-server 11 | - npm i 12 | - Start ws signaling server; 13 | - node signaling-server.js 14 | - Start answerer (On a new Console); 15 | - node client.js 16 | - Note local ID 17 | - Start Offerer (On a new Console); 18 | 19 | - node client.js 20 | - Enter answerer ID 21 | 22 | > You can also use [libdatachannel/examples/client](https://github.com/paullouisageneau/libdatachannel/tree/master/examples) project's client & signaling server 23 | -------------------------------------------------------------------------------- /examples/client-server/client-benchmark.js: -------------------------------------------------------------------------------- 1 | // createRequire is native in node version >= 12 2 | import { createRequire } from 'module'; 3 | const require = createRequire(import.meta.url); 4 | 5 | // yargs down not supports ES Modules 6 | const yargs = require('yargs/yargs'); 7 | const { hideBin } = require('yargs/helpers'); 8 | 9 | import readline from 'readline'; 10 | import nodeDataChannel from 'node-datachannel'; 11 | 12 | // Init Logger 13 | nodeDataChannel.initLogger('Info'); 14 | 15 | // PeerConnection Map 16 | const pcMap = {}; 17 | 18 | const dcArr = []; 19 | 20 | // Local ID 21 | const id = randomId(4); 22 | 23 | // Message Size 24 | const MESSAGE_SIZE = 65535; 25 | 26 | // Buffer Size 27 | const BUFFER_SIZE = MESSAGE_SIZE * 0; 28 | 29 | // Args 30 | const argv = yargs(hideBin(process.argv)) 31 | .option('disableSend', { 32 | type: 'boolean', 33 | description: 'Disable Send', 34 | default: false, 35 | }) 36 | .option('wsUrl', { 37 | type: 'string', 38 | description: 'Web Socket URL', 39 | default: 'ws://localhost:8000', 40 | }) 41 | .option('dataChannelCount', { 42 | type: 'number', 43 | description: 'Data Channel Count To Create', 44 | default: 1, 45 | }).argv; 46 | 47 | // Disable Send 48 | const disableSend = argv.disableSend; 49 | if (disableSend) console.log('Send Disabled!'); 50 | 51 | // Signaling Server 52 | const wsUrl = process.env.WS_URL || argv.wsUrl; 53 | console.log(wsUrl); 54 | const dataChannelCount = argv.dataChannelCount; 55 | 56 | // Read Line Interface 57 | const rl = readline.createInterface({ 58 | input: process.stdin, 59 | output: process.stdout, 60 | }); 61 | 62 | const ws = new nodeDataChannel.WebSocket(); 63 | ws.open(wsUrl + '/' + id); 64 | 65 | console.log(`The local ID is: ${id}`); 66 | console.log(`Waiting for signaling to be connected...`); 67 | 68 | ws.onOpen(() => { 69 | console.log('WebSocket connected, signaling ready'); 70 | readUserInput(); 71 | }); 72 | 73 | ws.onError((err) => { 74 | console.log('WebSocket Error: ', err); 75 | }); 76 | 77 | ws.onMessage((msgStr) => { 78 | let msg = JSON.parse(msgStr); 79 | switch (msg.type) { 80 | case 'offer': 81 | createPeerConnection(msg.id); 82 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 83 | break; 84 | case 'answer': 85 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 86 | break; 87 | case 'candidate': 88 | pcMap[msg.id].addRemoteCandidate(msg.candidate, msg.mid); 89 | break; 90 | 91 | default: 92 | break; 93 | } 94 | }); 95 | 96 | function readUserInput() { 97 | rl.question('Enter a remote ID to send an offer: ', (peerId) => { 98 | if (peerId && peerId.length > 2) { 99 | rl.close(); 100 | console.log('Offering to ', peerId); 101 | createPeerConnection(peerId); 102 | 103 | let msgToSend = randomId(MESSAGE_SIZE); 104 | 105 | for (let i = 1; i <= dataChannelCount; i++) { 106 | let dcArrItem = { 107 | dc: null, 108 | bytesSent: 0, 109 | bytesReceived: 0, 110 | }; 111 | console.log('Creating DataChannel with label "test-' + i + '"'); 112 | dcArrItem.dc = pcMap[peerId].createDataChannel('test-' + i); 113 | 114 | dcArrItem.dc.setBufferedAmountLowThreshold(BUFFER_SIZE); 115 | dcArrItem.dc.onOpen(() => { 116 | while (!disableSend && dcArrItem.dc.bufferedAmount() <= BUFFER_SIZE) { 117 | dcArrItem.dc.sendMessage(msgToSend); 118 | dcArrItem.bytesSent += msgToSend.length; 119 | } 120 | }); 121 | 122 | dcArrItem.dc.onBufferedAmountLow(() => { 123 | while (!disableSend && dcArrItem.dc.bufferedAmount() <= BUFFER_SIZE) { 124 | dcArrItem.dc.sendMessage(msgToSend); 125 | dcArrItem.bytesSent += msgToSend.length; 126 | } 127 | }); 128 | 129 | dcArrItem.dc.onMessage((msg) => { 130 | dcArrItem.bytesReceived += msg.length; 131 | }); 132 | 133 | dcArr.push(dcArrItem); 134 | } 135 | 136 | // Report 137 | let i = 0; 138 | setInterval(() => { 139 | let totalBytesSent = 0; 140 | let totalBytesReceived = 0; 141 | for (let j = 0; j < dcArr.length; j++) { 142 | console.log( 143 | `${j == 0 ? i + '#' : ''} DC-${j + 1} Sent: ${byte2KB( 144 | dcArr[j].bytesSent, 145 | )} KB/s / Received: ${byte2KB(dcArr[j].bytesReceived)} KB/s / SendBufferAmount: ${dcArr[ 146 | j 147 | ].dc.bufferedAmount()} / DataChannelOpen: ${dcArr[j].dc.isOpen()}`, 148 | ); 149 | totalBytesSent += dcArr[j].bytesSent; 150 | totalBytesReceived += dcArr[j].bytesReceived; 151 | dcArr[j].bytesSent = 0; 152 | dcArr[j].bytesReceived = 0; 153 | } 154 | console.log( 155 | `Total Sent: ${byte2KB(totalBytesSent)} KB/s / Total Received: ${byte2KB( 156 | totalBytesReceived, 157 | )} KB/s`, 158 | ); 159 | 160 | if (i % 5 == 0) { 161 | console.log( 162 | `Stats# Sent: ${byte2MB(pcMap[peerId].bytesSent())} MB / Received: ${byte2MB( 163 | pcMap[peerId].bytesReceived(), 164 | )} MB / rtt: ${pcMap[peerId].rtt()} ms`, 165 | ); 166 | console.log( 167 | `Selected Candidates# ${JSON.stringify(pcMap[peerId].getSelectedCandidatePair())}`, 168 | ); 169 | console.log(``); 170 | } 171 | i++; 172 | }, 1000); 173 | } 174 | }); 175 | } 176 | 177 | function createPeerConnection(peerId) { 178 | // Create PeerConnection 179 | let peerConnection = new nodeDataChannel.PeerConnection('pc', { 180 | iceServers: ['stun:stun.l.google.com:19302'], 181 | }); 182 | peerConnection.onStateChange((state) => { 183 | console.log('State: ', state); 184 | }); 185 | peerConnection.onGatheringStateChange((state) => { 186 | console.log('GatheringState: ', state); 187 | }); 188 | peerConnection.onLocalDescription((description, type) => { 189 | ws.sendMessage(JSON.stringify({ id: peerId, type, description })); 190 | }); 191 | peerConnection.onLocalCandidate((candidate, mid) => { 192 | ws.sendMessage(JSON.stringify({ id: peerId, type: 'candidate', candidate, mid })); 193 | }); 194 | peerConnection.onDataChannel((dc) => { 195 | rl.close(); 196 | console.log('DataChannel from ' + peerId + ' received with label "', dc.getLabel() + '"'); 197 | 198 | let msgToSend = randomId(MESSAGE_SIZE); 199 | 200 | while (!disableSend && dc.bufferedAmount() <= BUFFER_SIZE) { 201 | dc.sendMessage(msgToSend); 202 | } 203 | 204 | dc.onBufferedAmountLow(() => { 205 | while (!disableSend && dc.bufferedAmount() <= BUFFER_SIZE) { 206 | dc.sendMessage(msgToSend); 207 | } 208 | }); 209 | 210 | dc.onMessage((msg) => { 211 | // bytesReceived += msg.length; 212 | }); 213 | }); 214 | 215 | pcMap[peerId] = peerConnection; 216 | } 217 | 218 | function randomId(length) { 219 | var result = ''; 220 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 221 | var charactersLength = characters.length; 222 | for (var i = 0; i < length; i++) { 223 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 224 | } 225 | return result; 226 | } 227 | 228 | function byte2KB(bytes) { 229 | return `${Math.round(bytes / 1000)}`; 230 | } 231 | 232 | function byte2MB(bytes) { 233 | return `${Math.round(bytes / (1000 * 1000))}`; 234 | } 235 | -------------------------------------------------------------------------------- /examples/client-server/client-periodic.js: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | import nodeDataChannel from 'node-datachannel'; 3 | 4 | // Init Logger 5 | nodeDataChannel.initLogger('Info'); 6 | 7 | // PeerConnection Map 8 | const pcMap = {}; 9 | 10 | // Local ID 11 | const id = randomId(4); 12 | 13 | // Message Size 14 | const MESSAGE_SIZE = 500; 15 | 16 | // Buffer Size 17 | const BUFFER_SIZE = MESSAGE_SIZE * 10; 18 | 19 | // Read Line Interface 20 | const rl = readline.createInterface({ 21 | input: process.stdin, 22 | output: process.stdout, 23 | }); 24 | 25 | // Signaling Server 26 | const WS_URL = process.env.WS_URL || 'ws://localhost:8000'; 27 | const ws = new nodeDataChannel.WebSocket(); 28 | ws.open(WS_URL + '/' + id); 29 | 30 | console.log(`The local ID is: ${id}`); 31 | console.log(`Waiting for signaling to be connected...`); 32 | 33 | ws.onOpen(() => { 34 | console.log('WebSocket connected, signaling ready'); 35 | readUserInput(); 36 | }); 37 | 38 | ws.onError((err) => { 39 | console.log('WebSocket Error: ', err); 40 | }); 41 | 42 | ws.onMessage((msgStr) => { 43 | let msg = JSON.parse(msgStr); 44 | switch (msg.type) { 45 | case 'offer': 46 | createPeerConnection(msg.id); 47 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 48 | break; 49 | case 'answer': 50 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 51 | break; 52 | case 'candidate': 53 | pcMap[msg.id].addRemoteCandidate(msg.candidate, msg.mid); 54 | break; 55 | 56 | default: 57 | break; 58 | } 59 | }); 60 | 61 | function readUserInput() { 62 | rl.question('Enter a remote ID to send an offer: ', (peerId) => { 63 | if (peerId && peerId.length > 2) { 64 | rl.close(); 65 | console.log('Offering to ', peerId); 66 | createPeerConnection(peerId); 67 | 68 | console.log('Creating DataChannel with label "test"'); 69 | let dc = pcMap[peerId].createDataChannel('test'); 70 | 71 | let msgToSend = randomId(MESSAGE_SIZE); 72 | let bytesSent = 0; 73 | let bytesReceived = 0; 74 | 75 | dc.onOpen(() => { 76 | setInterval(() => { 77 | if (dc.bufferedAmount() <= BUFFER_SIZE) { 78 | dc.sendMessage(msgToSend); 79 | bytesSent += msgToSend.length; 80 | } 81 | }, 2); 82 | }); 83 | 84 | dc.onMessage((msg) => { 85 | bytesReceived += msg.length; 86 | }); 87 | 88 | // Report 89 | let i = 0; 90 | setInterval(() => { 91 | console.log( 92 | `${i++}# Sent: ${byte2KB(bytesSent)} KB/s / Received: ${byte2KB( 93 | bytesReceived, 94 | )} KB/s / SendBufferAmount: ${dc.bufferedAmount()} / DataChannelOpen: ${dc.isOpen()}`, 95 | ); 96 | bytesSent = 0; 97 | bytesReceived = 0; 98 | }, 1000); 99 | 100 | setInterval(() => { 101 | console.log( 102 | `Stats# Sent: ${byte2MB(pcMap[peerId].bytesSent())} MB / Received: ${byte2MB( 103 | pcMap[peerId].bytesReceived(), 104 | )} MB / rtt: ${pcMap[peerId].rtt()} ms`, 105 | ); 106 | console.log( 107 | `Selected Candidates# ${JSON.stringify(pcMap[peerId].getSelectedCandidatePair())}`, 108 | ); 109 | console.log(``); 110 | }, 5 * 1000); 111 | } 112 | }); 113 | } 114 | 115 | function createPeerConnection(peerId) { 116 | // Create PeerConnection 117 | let peerConnection = new nodeDataChannel.PeerConnection('pc', { 118 | iceServers: ['stun:stun.l.google.com:19302'], 119 | }); 120 | peerConnection.onStateChange((state) => { 121 | console.log('State: ', state); 122 | }); 123 | peerConnection.onGatheringStateChange((state) => { 124 | console.log('GatheringState: ', state); 125 | }); 126 | peerConnection.onLocalDescription((description, type) => { 127 | ws.sendMessage(JSON.stringify({ id: peerId, type, description })); 128 | }); 129 | peerConnection.onLocalCandidate((candidate, mid) => { 130 | ws.sendMessage(JSON.stringify({ id: peerId, type: 'candidate', candidate, mid })); 131 | }); 132 | peerConnection.onDataChannel((dc) => { 133 | rl.close(); 134 | console.log('DataChannel from ' + peerId + ' received with label "', dc.getLabel() + '"'); 135 | 136 | let msgToSend = randomId(MESSAGE_SIZE); 137 | let bytesSent = 0; 138 | let bytesReceived = 0; 139 | 140 | setInterval(() => { 141 | if (dc.bufferedAmount() <= BUFFER_SIZE) { 142 | dc.sendMessage(msgToSend); 143 | bytesSent += msgToSend.length; 144 | } 145 | }, 2); 146 | 147 | dc.onMessage((msg) => { 148 | bytesReceived += msg.length; 149 | }); 150 | 151 | // Report 152 | let i = 0; 153 | setInterval(() => { 154 | console.log( 155 | `${i++}# Sent: ${byte2KB(bytesSent)} KB/s / Received: ${byte2KB( 156 | bytesReceived, 157 | )} KB/s / SendBufferAmount: ${dc.bufferedAmount()} / DataChannelOpen: ${dc.isOpen()}`, 158 | ); 159 | bytesSent = 0; 160 | bytesReceived = 0; 161 | }, 1000); 162 | 163 | setInterval(() => { 164 | console.log( 165 | `Stats# Sent: ${byte2MB(pcMap[peerId].bytesSent())} MB / Received: ${byte2MB( 166 | pcMap[peerId].bytesReceived(), 167 | )} MB / rtt: ${pcMap[peerId].rtt()} ms`, 168 | ); 169 | console.log( 170 | `Selected Candidates# ${JSON.stringify(pcMap[peerId].getSelectedCandidatePair())}`, 171 | ); 172 | console.log(``); 173 | }, 5 * 1000); 174 | }); 175 | 176 | pcMap[peerId] = peerConnection; 177 | } 178 | 179 | function randomId(length) { 180 | var result = ''; 181 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 182 | var charactersLength = characters.length; 183 | for (var i = 0; i < length; i++) { 184 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 185 | } 186 | return result; 187 | } 188 | 189 | function byte2KB(bytes) { 190 | return `${Math.round(bytes / 1024)}`; 191 | } 192 | 193 | function byte2MB(bytes) { 194 | return `${Math.round(bytes / (1024 * 1024))}`; 195 | } 196 | -------------------------------------------------------------------------------- /examples/client-server/client.js: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | import nodeDataChannel from 'node-datachannel'; 3 | 4 | // Init Logger 5 | nodeDataChannel.initLogger('Error'); 6 | 7 | // PeerConnection Map 8 | const pcMap = {}; 9 | 10 | // Local ID 11 | const id = randomId(4); 12 | 13 | // Signaling Server 14 | const WS_URL = process.env.WS_URL || 'ws://localhost:8000'; 15 | const ws = new nodeDataChannel.WebSocket(); 16 | ws.open(WS_URL + '/' + id); 17 | 18 | console.log(`The local ID is: ${id}`); 19 | console.log(`Waiting for signaling to be connected...`); 20 | 21 | ws.onOpen(() => { 22 | console.log('WebSocket connected, signaling ready'); 23 | readUserInput(); 24 | }); 25 | 26 | ws.onError((err) => { 27 | console.log('WebSocket Error: ', err); 28 | }); 29 | 30 | ws.onMessage((msgStr) => { 31 | let msg = JSON.parse(msgStr); 32 | switch (msg.type) { 33 | case 'offer': 34 | createPeerConnection(msg.id); 35 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 36 | break; 37 | case 'answer': 38 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 39 | break; 40 | case 'candidate': 41 | pcMap[msg.id].addRemoteCandidate(msg.candidate, msg.mid); 42 | break; 43 | 44 | default: 45 | break; 46 | } 47 | }); 48 | 49 | function readUserInput() { 50 | // Read Line Interface 51 | const rl = readline.createInterface({ 52 | input: process.stdin, 53 | output: process.stdout, 54 | }); 55 | 56 | rl.question('Enter a remote ID to send an offer:\n', (peerId) => { 57 | if (peerId && peerId.length > 2) { 58 | console.log('Offering to ', peerId); 59 | createPeerConnection(peerId); 60 | 61 | console.log('Creating DataChannel with label "test"'); 62 | let dc = pcMap[peerId].createDataChannel('test'); 63 | dc.onOpen(() => { 64 | dc.sendMessage('Hello from ' + id); 65 | }); 66 | 67 | dc.onMessage((msg) => { 68 | console.log('Message from ' + peerId + ' received:', msg); 69 | }); 70 | } 71 | 72 | rl.close(); 73 | readUserInput(); 74 | }); 75 | } 76 | 77 | function createPeerConnection(peerId) { 78 | // Create PeerConnection 79 | let peerConnection = new nodeDataChannel.PeerConnection('pc', { 80 | iceServers: ['stun:stun.l.google.com:19302'], 81 | }); 82 | peerConnection.onStateChange((state) => { 83 | console.log('State: ', state); 84 | }); 85 | peerConnection.onGatheringStateChange((state) => { 86 | console.log('GatheringState: ', state); 87 | }); 88 | peerConnection.onLocalDescription((description, type) => { 89 | ws.sendMessage(JSON.stringify({ id: peerId, type, description })); 90 | }); 91 | peerConnection.onLocalCandidate((candidate, mid) => { 92 | ws.sendMessage(JSON.stringify({ id: peerId, type: 'candidate', candidate, mid })); 93 | }); 94 | peerConnection.onDataChannel((dc) => { 95 | console.log('DataChannel from ' + peerId + ' received with label "', dc.getLabel() + '"'); 96 | dc.onMessage((msg) => { 97 | console.log('Message from ' + peerId + ' received:', msg); 98 | }); 99 | dc.sendMessage('Hello From ' + id); 100 | }); 101 | 102 | pcMap[peerId] = peerConnection; 103 | } 104 | 105 | function randomId(length) { 106 | var result = ''; 107 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 108 | var charactersLength = characters.length; 109 | for (var i = 0; i < length; i++) { 110 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 111 | } 112 | return result; 113 | } 114 | -------------------------------------------------------------------------------- /examples/client-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "client.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../..", 15 | "yargs": "^16.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/client-server/signaling-server.js: -------------------------------------------------------------------------------- 1 | import nodeDataChannel from 'node-datachannel'; 2 | 3 | // Init Logger 4 | nodeDataChannel.initLogger('Debug'); 5 | 6 | const clients = {}; 7 | 8 | const wsServer = new nodeDataChannel.WebSocketServer({ 9 | bindAddress: '127.0.0.1', 10 | port: 8000, 11 | }); 12 | 13 | wsServer.onClient((ws) => { 14 | let id = ''; 15 | 16 | ws.onOpen(() => { 17 | id = ws.path().replace('/', ''); 18 | console.log(`New Connection from ${id}`); 19 | clients[id] = ws; 20 | }); 21 | 22 | ws.onMessage((buffer) => { 23 | let msg = JSON.parse(buffer); 24 | let peerId = msg.id; 25 | let peerWs = clients[peerId]; 26 | 27 | console.log(`Message from ${id} to ${peerId} : ${buffer}`); 28 | if (!peerWs) return console.error(`Can not find peer with ID ${peerId}`); 29 | 30 | msg.id = id; 31 | peerWs.sendMessage(JSON.stringify(msg)); 32 | }); 33 | 34 | ws.onClosed(() => { 35 | console.log(`${id} disconnected`); 36 | delete clients[id]; 37 | }); 38 | 39 | ws.onError((err) => { 40 | console.error(err); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/electron-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Vite 89 | .vite/ 90 | 91 | # Electron-Forge 92 | out/ 93 | -------------------------------------------------------------------------------- /examples/electron-demo/README.md: -------------------------------------------------------------------------------- 1 | # node-datachannel electron demo 2 | 3 | Project created with 4 | 5 | > `npm init electron-app@latest my-new-app -- --template=webpack` 6 | 7 | ## Electron Compatibility and Rebuilding 8 | 9 | `node-datachannel` uses N-API for creating binaries. Normally `electron` is compatible with N-API binaries, so you should not need to rebuild the binaries. 10 | 11 | ### If you need anyway, you can use `electron-rebuild` package with some modifications. 12 | 13 | Electron does not support `cmake-js` which `node-datachannel` uses as builder. 14 | 15 | As a workaround you can use a custom plugin for `electron-forge`. 16 | 17 | Package named as [plugin-node-datachannel](https://github.com/murat-dogan/plugin-node-datachannel) 18 | 19 | For rebuilding native addon; 20 | 21 | ``` 22 | > npm i https://github.com/murat-dogan/plugin-node-datachannel 23 | 24 | # Add plugin to forge.config.js like below 25 | ... 26 | { 27 | name: 'plugin-node-datachannel', 28 | config: {}, 29 | }, 30 | ... 31 | ``` 32 | 33 | ## How to start the demo Application 34 | 35 | ### Package the App 36 | 37 | - cd examples/electron-demo 38 | - npm install 39 | - npm run package 40 | 41 | Now the app is built and ready to be used in folder `out`. 42 | 43 | ### Start Signaling Server 44 | 45 | In another terminal, start signaling server as below; 46 | 47 | - cd examples/client-server 48 | - npm install 49 | - node signaling-server.js 50 | 51 | Now signaling server is ready and running. 52 | 53 | ### Start App 54 | 55 | - Start 2 instances of the application 56 | - Copy ID of one instance 57 | - Paste the copied ID to the other instance 58 | - Click Connect 59 | -------------------------------------------------------------------------------- /examples/electron-demo/forge.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packagerConfig: { 3 | asar: true, 4 | }, 5 | rebuildConfig: {}, 6 | makers: [ 7 | { 8 | name: '@electron-forge/maker-squirrel', 9 | config: {}, 10 | }, 11 | { 12 | name: '@electron-forge/maker-zip', 13 | platforms: ['darwin'], 14 | }, 15 | { 16 | name: '@electron-forge/maker-deb', 17 | config: {}, 18 | }, 19 | { 20 | name: '@electron-forge/maker-rpm', 21 | config: {}, 22 | }, 23 | ], 24 | plugins: [ 25 | { 26 | name: '@electron-forge/plugin-auto-unpack-natives', 27 | config: {}, 28 | }, 29 | { 30 | name: '@electron-forge/plugin-webpack', 31 | config: { 32 | mainConfig: './webpack.main.config.js', 33 | renderer: { 34 | config: './webpack.renderer.config.js', 35 | entryPoints: [ 36 | { 37 | html: './src/index.html', 38 | js: './src/renderer.js', 39 | name: 'main_window', 40 | preload: { 41 | js: './src/preload.js', 42 | }, 43 | }, 44 | ], 45 | }, 46 | }, 47 | }, 48 | ], 49 | }; 50 | -------------------------------------------------------------------------------- /examples/electron-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-demo", 3 | "productName": "electron-demo", 4 | "version": "1.0.0", 5 | "description": "My Electron application description", 6 | "main": ".webpack/main", 7 | "scripts": { 8 | "start": "electron-forge start", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make", 11 | "publish": "electron-forge publish", 12 | "lint": "echo \"No linting configured\"" 13 | }, 14 | "keywords": [], 15 | "author": { 16 | "name": "Murat Dogan", 17 | "email": "doganmurat@gmail.com" 18 | }, 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@electron-forge/cli": "^6.4.2", 22 | "@electron-forge/maker-deb": "^6.4.2", 23 | "@electron-forge/maker-rpm": "^6.4.2", 24 | "@electron-forge/maker-squirrel": "^6.4.2", 25 | "@electron-forge/maker-zip": "^6.4.2", 26 | "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", 27 | "@electron-forge/plugin-webpack": "^6.4.2", 28 | "@electron/rebuild": "^3.3.0", 29 | "@vercel/webpack-asset-relocator-loader": "^1.7.3", 30 | "copy-webpack-plugin": "^12.0.2", 31 | "css-loader": "^6.8.1", 32 | "electron": "26.2.4", 33 | "node-loader": "^2.0.0", 34 | "style-loader": "^3.3.3" 35 | }, 36 | "dependencies": { 37 | "electron-squirrel-startup": "^1.0.0", 38 | "node-datachannel": "^0.12.0" 39 | }, 40 | "cmake-js": { 41 | "runtime": "electron", 42 | "runtimeVersion": "26.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/electron-demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 3 | margin: auto; 4 | max-width: 38rem; 5 | padding: 2rem; 6 | } 7 | -------------------------------------------------------------------------------- /examples/electron-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | node-datachannel Demo 6 | 7 | 8 |

node-datachannel Demo

9 |

My ID:

10 |
11 | 12 | 13 | 14 |
15 |

Status: disconnected

16 |
17 |
18 |

Messages

19 | 20 | 21 |
22 |
23 | Send Random Messages 24 | Every ms 25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/electron-demo/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { app, BrowserWindow } = require('electron'); 3 | 4 | const { run } = require('./node-datachannel.js'); 5 | 6 | // Handle creating/removing shortcuts on Windows when installing/uninstalling. 7 | if (require('electron-squirrel-startup')) { 8 | app.quit(); 9 | } 10 | 11 | const createWindow = () => { 12 | // Create the browser window. 13 | const mainWindow = new BrowserWindow({ 14 | width: 800, 15 | height: 600, 16 | webPreferences: { 17 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, 18 | }, 19 | }); 20 | 21 | // and load the index.html of the app. 22 | mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); 23 | 24 | // Open the DevTools. 25 | mainWindow.webContents.openDevTools(); 26 | 27 | run(mainWindow); 28 | }; 29 | 30 | // This method will be called when Electron has finished 31 | // initialization and is ready to create browser windows. 32 | // Some APIs can only be used after this event occurs. 33 | app.on('ready', createWindow); 34 | 35 | // Quit when all windows are closed, except on macOS. There, it's common 36 | // for applications and their menu bar to stay active until the user quits 37 | // explicitly with Cmd + Q. 38 | app.on('window-all-closed', () => { 39 | if (process.platform !== 'darwin') { 40 | app.quit(); 41 | } 42 | }); 43 | 44 | app.on('activate', () => { 45 | // On OS X it's common to re-create a window in the app when the 46 | // dock icon is clicked and there are no other windows open. 47 | if (BrowserWindow.getAllWindows().length === 0) { 48 | createWindow(); 49 | } 50 | }); 51 | 52 | // In this file you can include the rest of your app's specific main process 53 | // code. You can also put them in separate files and import them here. 54 | -------------------------------------------------------------------------------- /examples/electron-demo/src/node-datachannel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const nodeDataChannel = require('node-datachannel'); 3 | const WebSocket = require('ws'); 4 | const { ipcMain } = require('electron'); 5 | 6 | export function run(mainWindow) { 7 | nodeDataChannel.initLogger('Debug'); 8 | nodeDataChannel.preload(); 9 | 10 | // My ID 11 | let myId = getRandomString(4); 12 | let _remoteId = ''; 13 | mainWindow.webContents.send('my-id', myId); 14 | 15 | // PeerConnection Map 16 | const pcMap = {}; 17 | const dcMap = {}; 18 | 19 | // Signaling Server 20 | const WS_URL = process.env.WS_URL || 'ws://localhost:8000'; 21 | const ws = new WebSocket(WS_URL + '/' + myId, { 22 | perMessageDeflate: false, 23 | }); 24 | 25 | ws.on('open', () => { 26 | console.log('WebSocket connected, signaling ready'); 27 | }); 28 | 29 | ws.on('error', (err) => { 30 | console.log('WebSocket Error: ', err); 31 | }); 32 | 33 | ws.on('message', (msgStr) => { 34 | let msg = JSON.parse(msgStr); 35 | switch (msg.type) { 36 | case 'offer': 37 | _remoteId = msg.id; 38 | mainWindow.webContents.send('connect', _remoteId); 39 | createPeerConnection(msg.id); 40 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 41 | break; 42 | case 'answer': 43 | pcMap[msg.id].setRemoteDescription(msg.description, msg.type); 44 | break; 45 | case 'candidate': 46 | pcMap[msg.id].addRemoteCandidate(msg.candidate, msg.mid); 47 | break; 48 | 49 | default: 50 | break; 51 | } 52 | }); 53 | 54 | ipcMain.on('connect', (e, remoteId) => { 55 | _remoteId = remoteId; 56 | createPeerConnection(remoteId); 57 | dcMap[remoteId] = pcMap[remoteId].createDataChannel('chat'); 58 | dcMap[remoteId].onOpen(() => { 59 | mainWindow.webContents.send('message', remoteId, '[DataChannel opened]'); 60 | }); 61 | 62 | dcMap[remoteId].onMessage((msg) => { 63 | console.log('Message from ' + remoteId + ' received:', msg); 64 | // Add message to textarea 65 | mainWindow.webContents.send('message', remoteId, msg); 66 | }); 67 | }); 68 | 69 | ipcMain.on('send-message', (e, remoteId, msg) => { 70 | dcMap[remoteId].sendMessage(msg); 71 | }); 72 | 73 | let intervalref = null; 74 | ipcMain.on('send-random-message', (e, enabled, interval) => { 75 | if (intervalref) clearInterval(intervalref); 76 | if (!enabled) return; 77 | 78 | intervalref = setInterval(() => { 79 | if (_remoteId && _remoteId.length > 2) { 80 | let msg = getRandomString(10); 81 | mainWindow.webContents.send('message', 'me', msg); 82 | dcMap[_remoteId].sendMessage(msg); 83 | } 84 | }, interval); 85 | }); 86 | 87 | function createPeerConnection(peerId) { 88 | // Create PeerConnection 89 | let peerConnection = new nodeDataChannel.PeerConnection(myId, { 90 | iceServers: ['stun:stun.l.google.com:19302'], 91 | }); 92 | peerConnection.onStateChange((state) => { 93 | console.log('State: ', state); 94 | // set state value 95 | mainWindow.webContents.send('state-update', state); 96 | }); 97 | peerConnection.onGatheringStateChange((state) => { 98 | console.log('GatheringState: ', state); 99 | }); 100 | peerConnection.onLocalDescription((description, type) => { 101 | ws.send(JSON.stringify({ id: peerId, type, description })); 102 | }); 103 | peerConnection.onLocalCandidate((candidate, mid) => { 104 | ws.send(JSON.stringify({ id: peerId, type: 'candidate', candidate, mid })); 105 | }); 106 | peerConnection.onDataChannel((dc) => { 107 | console.log('DataChannel from ' + peerId + ' received with label "', dc.getLabel() + '"'); 108 | mainWindow.webContents.send('message', peerId, '[DataChannel opened]'); 109 | dcMap[peerId] = dc; 110 | dc.onMessage((msg) => { 111 | console.log('Message from ' + peerId + ' received:', msg); 112 | // Add message to textarea 113 | mainWindow.webContents.send('message', peerId, msg); 114 | }); 115 | }); 116 | 117 | pcMap[peerId] = peerConnection; 118 | } 119 | 120 | function getRandomString(length) { 121 | return Math.random() 122 | .toString(36) 123 | .substring(2, 2 + length); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /examples/electron-demo/src/preload.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // See the Electron documentation for details on how to use preload scripts: 3 | // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts 4 | const { contextBridge, ipcRenderer } = require('electron'); 5 | 6 | contextBridge.exposeInMainWorld('electron', { 7 | connect: (remoteId) => ipcRenderer.send('connect', remoteId), 8 | sendMessage: (remoteId, msg) => ipcRenderer.send('send-message', remoteId, msg), 9 | sendRandomMessage: (enabled, interval) => 10 | ipcRenderer.send('send-random-message', enabled, interval), 11 | }); 12 | 13 | ipcRenderer.on('my-id', (e, myId) => { 14 | document.getElementById('my-id').innerHTML = myId; 15 | }); 16 | 17 | ipcRenderer.on('message', (e, remoteId, msg) => { 18 | document.getElementById('messages').value += remoteId + '> ' + msg + '\n'; 19 | }); 20 | 21 | ipcRenderer.on('connect', (e, remoteId) => { 22 | document.getElementById('connect').disabled = true; 23 | document.getElementById('remote-id').disabled = true; 24 | document.getElementById('remote-id').value = remoteId; 25 | }); 26 | 27 | ipcRenderer.on('state-update', (e, state) => { 28 | document.getElementById('state').innerHTML = state; 29 | }); 30 | -------------------------------------------------------------------------------- /examples/electron-demo/src/renderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file will automatically be loaded by webpack and run in the "renderer" context. 3 | * To learn more about the differences between the "main" and the "renderer" context in 4 | * Electron, visit: 5 | * 6 | * https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 7 | * 8 | * By default, Node.js integration in this file is disabled. When enabling Node.js integration 9 | * in a renderer process, please be aware of potential security implications. You can read 10 | * more about security risks here: 11 | * 12 | * https://electronjs.org/docs/tutorial/security 13 | * 14 | * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration` 15 | * flag: 16 | * 17 | * ``` 18 | * // Create the browser window. 19 | * mainWindow = new BrowserWindow({ 20 | * width: 800, 21 | * height: 600, 22 | * webPreferences: { 23 | * nodeIntegration: true 24 | * } 25 | * }); 26 | * ``` 27 | */ 28 | 29 | import './index.css'; 30 | 31 | window.connect = () => { 32 | const remoteId = document.getElementById('remote-id').value; 33 | if (remoteId && remoteId.length > 2) { 34 | // disable connect button 35 | document.getElementById('connect').disabled = true; 36 | document.getElementById('remote-id').disabled = true; 37 | 38 | window.electron.connect(remoteId); 39 | } 40 | }; 41 | 42 | window.sendMessage = () => { 43 | const remoteId = document.getElementById('remote-id').value; 44 | if (remoteId && remoteId.length > 2) { 45 | let msg = document.getElementById('new-message').value; 46 | if (msg && msg.length > 0) { 47 | document.getElementById('new-message').value = ''; 48 | document.getElementById('messages').value += 'me> ' + msg + '\n'; 49 | window.electron.sendMessage(remoteId, msg); 50 | } 51 | } 52 | }; 53 | 54 | window.sendRandomMessage = () => { 55 | let enabled = document.getElementById('sendRandom').checked; 56 | let interval = document.getElementById('randomInterval').value; 57 | window.electron.sendRandomMessage(enabled, interval); 58 | }; 59 | -------------------------------------------------------------------------------- /examples/electron-demo/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require('copy-webpack-plugin'); 2 | 3 | module.exports = { 4 | /** 5 | * This is the main entry point for your application, it's the first file 6 | * that runs in the main process. 7 | */ 8 | entry: './src/main.js', 9 | // Put your normal webpack config below here 10 | module: { 11 | rules: require('./webpack.rules'), 12 | }, 13 | externals: { 14 | 'node-datachannel': 'node-datachannel', 15 | }, 16 | plugins: [ 17 | new CopyPlugin({ 18 | patterns: [ 19 | { 20 | from: 'node_modules/node-datachannel', 21 | to: 'node_modules/node-datachannel', 22 | }, 23 | ], 24 | }), 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /examples/electron-demo/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | const rules = require('./webpack.rules'); 2 | 3 | rules.push({ 4 | test: /\.css$/, 5 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 6 | }); 7 | 8 | module.exports = { 9 | // Put your normal webpack config below here 10 | module: { 11 | rules, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /examples/electron-demo/webpack.rules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // Add support for native node modules 3 | { 4 | // We're specifying native_modules in the test because the asset relocator loader generates a 5 | // "fake" .node file which is really a cjs file. 6 | test: /native_modules[/\\].+\.node$/, 7 | use: 'node-loader', 8 | }, 9 | { 10 | test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, 11 | parser: { amd: false }, 12 | use: { 13 | loader: '@vercel/webpack-asset-relocator-loader', 14 | options: { 15 | outputAssetBase: 'native_modules', 16 | }, 17 | }, 18 | }, 19 | // Put your webpack loader rules in this array. This is where you would put 20 | // your ts-loader configuration for instance: 21 | /** 22 | * Typescript Example: 23 | * 24 | * { 25 | * test: /\.tsx?$/, 26 | * exclude: /(node_modules|.webpack)/, 27 | * loaders: [{ 28 | * loader: 'ts-loader', 29 | * options: { 30 | * transpileOnly: true 31 | * } 32 | * }] 33 | * } 34 | */ 35 | ]; 36 | -------------------------------------------------------------------------------- /examples/media/README.md: -------------------------------------------------------------------------------- 1 | # media Example 2 | 3 | ## Example Webcam from Browser to Port 5000 4 | 5 | This is an example copy/paste demo to send your webcam from your browser and out port 5000 through the demo application. 6 | 7 | ## How to use 8 | 9 | Open main.html in your browser (you must open it either as HTTPS or as a domain of http://localhost). 10 | 11 | Start the application and copy it's offer into the text box of the web page. 12 | 13 | Copy the answer of the webpage back into the application. 14 | 15 | You will now see RTP traffic on `localhost:5000` of the computer that the application is running on. 16 | 17 | Use the following gstreamer demo pipeline to display the traffic 18 | (you might need to wave your hand in front of your camera to force an I-frame). 19 | 20 | ``` 21 | $ gst-launch-1.0 udpsrc address=127.0.0.1 port=5000 caps="application/x-rtp" ! queue ! rtph264depay ! video/x-h264,stream-format=byte-stream ! queue ! avdec_h264 ! queue ! autovideosink 22 | ``` 23 | 24 | For saving the stream to a file, use the following pipeline (mp4): 25 | 26 | ``` 27 | gst-launch-1.0 -e udpsrc address=127.0.0.1 port=5000 caps="application/x-rtp" ! queue ! rtph264depay ! h264parse ! mp4mux ! filesink location=out.mp4 28 | ``` 29 | 30 | ## Requirements 31 | 32 | - GStreamer 1.0 33 | - gstreamer1.0-libav (sudo apt-get install gstreamer1.0-libav) 34 | - h264enc (sudo apt-get install h264enc) 35 | -------------------------------------------------------------------------------- /examples/media/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | libdatachannel media example 6 | 7 | 8 |

Please enter the offer provided to you by the application:

9 | 10 | 11 | 12 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/media/media.js: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | import nodeDataChannel from 'node-datachannel'; 3 | import dgram from 'dgram'; 4 | 5 | var client = dgram.createSocket('udp4'); 6 | 7 | // Read Line Interface 8 | const rl = readline.createInterface({ 9 | input: process.stdin, 10 | output: process.stdout, 11 | }); 12 | 13 | // Init Logger 14 | nodeDataChannel.initLogger('Debug'); 15 | 16 | let peerConnection = new nodeDataChannel.PeerConnection('pc', { 17 | iceServers: [], 18 | }); 19 | 20 | peerConnection.onStateChange((state) => { 21 | console.log('State: ', state); 22 | }); 23 | peerConnection.onGatheringStateChange((state) => { 24 | // console.log('GatheringState: ', state); 25 | 26 | if (state == 'complete') { 27 | let desc = peerConnection.localDescription(); 28 | console.log(''); 29 | console.log('## Please copy the offer below to the web page:'); 30 | console.log(JSON.stringify(desc)); 31 | console.log('\n\n'); 32 | console.log('## Expect RTP video traffic on localhost:5000'); 33 | rl.question('## Please copy/paste the answer provided by the browser: \n', (sdp) => { 34 | let sdpObj = JSON.parse(sdp); 35 | peerConnection.setRemoteDescription(sdpObj.sdp, sdpObj.type); 36 | console.log(track.isOpen()); 37 | rl.close(); 38 | }); 39 | } 40 | }); 41 | 42 | let video = new nodeDataChannel.Video('video', 'RecvOnly'); 43 | video.addH264Codec(96); 44 | video.setBitrate(3000); 45 | 46 | let track = peerConnection.addTrack(video); 47 | track.onMessage((msg) => { 48 | client.send(msg, 5000, '127.0.0.1', (err, n) => { 49 | if (err) console.log(err, n); 50 | }); 51 | }); 52 | 53 | peerConnection.setLocalDescription(); 54 | -------------------------------------------------------------------------------- /examples/media/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "media", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "## Example Webcam from Browser to Port 5000", 6 | "main": "media.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple-peer-usage/main.js: -------------------------------------------------------------------------------- 1 | import Peer from 'simple-peer'; 2 | import nodeDatachannelPolyfill from 'node-datachannel/polyfill'; 3 | import nodeDataChannel from 'node-datachannel'; 4 | 5 | nodeDataChannel.initLogger('Info'); 6 | 7 | var peer1 = new Peer({ initiator: true, wrtc: nodeDatachannelPolyfill }); 8 | var peer2 = new Peer({ wrtc: nodeDatachannelPolyfill }); 9 | 10 | peer1.on('signal', (data) => { 11 | // when peer1 has signaling data, give it to peer2 somehow 12 | peer2.signal(data); 13 | }); 14 | 15 | peer2.on('signal', (data) => { 16 | // when peer2 has signaling data, give it to peer1 somehow 17 | peer1.signal(data); 18 | }); 19 | 20 | peer1.on('connect', () => { 21 | // wait for 'connect' event before using the data channel 22 | peer1.send('hey peer2, how is it going?'); 23 | }); 24 | 25 | peer2.on('data', (data) => { 26 | // got a data channel message 27 | console.log('got a message from peer1: ' + data); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/simple-peer-usage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-peer-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../..", 15 | "simple-peer": "^9.11.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/use-cases/commonjs/index.js: -------------------------------------------------------------------------------- 1 | const nodeDataChannel = require('node-datachannel'); 2 | const { RTCPeerConnection } = require('node-datachannel/polyfill'); 3 | 4 | nodeDataChannel.initLogger('Debug'); 5 | 6 | // Create a new PeerConnection object 7 | const peer1 = new nodeDataChannel.PeerConnection('Peer1', { 8 | iceServers: ['stun:stun.l.google.com:19302'], 9 | }); 10 | peer1.close(); 11 | 12 | // Polyfill PeerConnection 13 | const peer2 = new RTCPeerConnection({ 14 | peerIdentity: 'Peer2', 15 | iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], 16 | }); 17 | peer2.close(); 18 | 19 | // Cleanup the node-datachannel library 20 | nodeDataChannel.cleanup(); 21 | -------------------------------------------------------------------------------- /examples/use-cases/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commonjs", 3 | "version": "1.0.0", 4 | "type": "commonjs", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/use-cases/esm/index.js: -------------------------------------------------------------------------------- 1 | import nodeDataChannel from 'node-datachannel'; 2 | import { RTCPeerConnection } from 'node-datachannel/polyfill'; 3 | 4 | nodeDataChannel.initLogger('Debug'); 5 | 6 | // Create a new PeerConnection object 7 | const peer1 = new nodeDataChannel.PeerConnection('Peer1', { 8 | iceServers: ['stun:stun.l.google.com:19302'], 9 | }); 10 | peer1.close(); 11 | 12 | // Polyfill PeerConnection 13 | const peer2 = new RTCPeerConnection({ 14 | peerIdentity: 'Peer2', 15 | iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], 16 | }); 17 | peer2.close(); 18 | 19 | // Cleanup the node-datachannel library 20 | nodeDataChannel.cleanup(); 21 | -------------------------------------------------------------------------------- /examples/use-cases/esm/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "esm", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "node-datachannel": "file:../../.." 13 | } 14 | }, 15 | "../../..": { 16 | "version": "0.20.0-dev", 17 | "hasInstallScript": true, 18 | "license": "MPL 2.0", 19 | "dependencies": { 20 | "node-domexception": "^2.0.1", 21 | "prebuild-install": "^7.0.1" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^29.5.12", 25 | "@types/node": "^20.6.1", 26 | "@typescript-eslint/eslint-plugin": "^7.17.0", 27 | "@typescript-eslint/parser": "^7.17.0", 28 | "cmake-js": "^6.3.2", 29 | "eslint": "^8.57.0", 30 | "eslint-config-prettier": "^9.1.0", 31 | "eslint-plugin-jest": "^28.6.0", 32 | "eslint-plugin-prettier": "^5.2.1", 33 | "jest": "^29.7.0", 34 | "jsdom": "^24.1.1", 35 | "node-addon-api": "^7.0.0", 36 | "prebuild": "^12.0.0", 37 | "prettier": "^3.3.3", 38 | "puppeteer": "^22.14.0", 39 | "rimraf": "^5.0.9", 40 | "rollup": "^4.14.1", 41 | "ts-api-utils": "^1.3.0", 42 | "ts-jest": "^29.2.3", 43 | "ts-node": "^10.9.2", 44 | "typescript": "5.4" 45 | }, 46 | "engines": { 47 | "node": ">=16.0.0" 48 | } 49 | }, 50 | "node_modules/node-datachannel": { 51 | "resolved": "../../..", 52 | "link": true 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/use-cases/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/use-cases/typescript/index.ts: -------------------------------------------------------------------------------- 1 | import nodeDataChannel from 'node-datachannel'; 2 | import { RTCPeerConnection } from 'node-datachannel/polyfill'; 3 | 4 | nodeDataChannel.initLogger('Debug'); 5 | 6 | // Create a new PeerConnection object 7 | const peer1 = new nodeDataChannel.PeerConnection('Peer1', { 8 | iceServers: ['stun:stun.l.google.com:19302'], 9 | }); 10 | peer1.close(); 11 | 12 | // Polyfill PeerConnection 13 | const peer2 = new RTCPeerConnection({ 14 | peerIdentity: 'Peer2', 15 | iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], 16 | }); 17 | peer2.close(); 18 | 19 | // Cleanup the node-datachannel library 20 | nodeDataChannel.cleanup(); 21 | -------------------------------------------------------------------------------- /examples/use-cases/typescript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "esm", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "node-datachannel": "file:../../.." 13 | } 14 | }, 15 | "../../..": { 16 | "version": "0.20.0-dev", 17 | "hasInstallScript": true, 18 | "license": "MPL 2.0", 19 | "dependencies": { 20 | "node-domexception": "^2.0.1", 21 | "prebuild-install": "^7.0.1" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-esm-shim": "^0.1.7", 25 | "@types/jest": "^29.5.12", 26 | "@types/node": "^20.6.1", 27 | "@typescript-eslint/eslint-plugin": "^7.17.0", 28 | "@typescript-eslint/parser": "^7.17.0", 29 | "cmake-js": "^6.3.2", 30 | "eslint": "^8.57.0", 31 | "eslint-config-prettier": "^9.1.0", 32 | "eslint-plugin-jest": "^28.6.0", 33 | "eslint-plugin-prettier": "^5.2.1", 34 | "jest": "^29.7.0", 35 | "jsdom": "^24.1.1", 36 | "node-addon-api": "^7.0.0", 37 | "prebuild": "^12.0.0", 38 | "prettier": "^3.3.3", 39 | "puppeteer": "^22.14.0", 40 | "rimraf": "^5.0.9", 41 | "rollup": "^4.19.1", 42 | "rollup-plugin-dts": "^6.1.1", 43 | "rollup-plugin-esbuild": "^6.1.1", 44 | "ts-api-utils": "^1.3.0", 45 | "ts-jest": "^29.2.3", 46 | "ts-node": "^10.9.2", 47 | "typescript": "5.4" 48 | }, 49 | "engines": { 50 | "node": ">=16.0.0" 51 | } 52 | }, 53 | "node_modules/node-datachannel": { 54 | "resolved": "../../..", 55 | "link": true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/use-cases/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/use-cases/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleResolution": "NodeNext", 5 | "rootDir": ".", 6 | "outDir": "./dist", 7 | "module": "NodeNext" 8 | }, 9 | "include": ["index.ts"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/websocket/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "websocket", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "node-datachannel": "file:../.." 13 | } 14 | }, 15 | "../..": { 16 | "version": "0.20.0-dev", 17 | "hasInstallScript": true, 18 | "license": "MPL 2.0", 19 | "dependencies": { 20 | "node-domexception": "^2.0.1", 21 | "prebuild-install": "^7.0.1" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-esm-shim": "^0.1.7", 25 | "@types/jest": "^29.5.12", 26 | "@types/node": "^20.6.1", 27 | "@typescript-eslint/eslint-plugin": "^7.17.0", 28 | "@typescript-eslint/parser": "^7.17.0", 29 | "cmake-js": "^6.3.2", 30 | "eslint": "^8.57.0", 31 | "eslint-config-prettier": "^9.1.0", 32 | "eslint-plugin-jest": "^28.6.0", 33 | "eslint-plugin-prettier": "^5.2.1", 34 | "jest": "^29.7.0", 35 | "jsdom": "^24.1.1", 36 | "node-addon-api": "^7.0.0", 37 | "prebuild": "^12.0.0", 38 | "prettier": "^3.3.3", 39 | "puppeteer": "^22.14.0", 40 | "rimraf": "^5.0.9", 41 | "rollup": "^4.19.1", 42 | "rollup-plugin-dts": "^6.1.1", 43 | "rollup-plugin-esbuild": "^6.1.1", 44 | "ts-api-utils": "^1.3.0", 45 | "ts-jest": "^29.2.3", 46 | "ts-node": "^10.9.2", 47 | "typescript": "5.4" 48 | }, 49 | "engines": { 50 | "node": ">=16.0.0" 51 | } 52 | }, 53 | "node_modules/node-datachannel": { 54 | "resolved": "../..", 55 | "link": true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "", 6 | "main": "websocket-client.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/websocket/websocket-client.js: -------------------------------------------------------------------------------- 1 | import nodeDataChannel from 'node-datachannel'; 2 | 3 | const clientSocket = new nodeDataChannel.WebSocket(); 4 | 5 | clientSocket.open('ws://127.0.0.1:3000'); 6 | 7 | clientSocket.onOpen(() => { 8 | console.log('Client socket opened'); 9 | clientSocket.sendMessage('Echo this!'); 10 | }); 11 | 12 | clientSocket.onMessage((message) => { 13 | console.log('Message from server: ' + message); 14 | }); 15 | 16 | clientSocket.onClosed(() => { 17 | console.log('Client socket closed'); 18 | }); 19 | 20 | // clientSocket.close(); 21 | -------------------------------------------------------------------------------- /examples/websocket/websocket-server.js: -------------------------------------------------------------------------------- 1 | import nodeDataChannel from 'node-datachannel'; 2 | 3 | const ws = new nodeDataChannel.WebSocketServer({ 4 | bindAddress: '127.0.0.1', 5 | port: 3000, 6 | }); 7 | 8 | ws.onClient((clientSocket) => { 9 | clientSocket.onOpen(() => { 10 | console.log('Client socket opened'); 11 | clientSocket.sendMessage('Hello from server'); 12 | }); 13 | 14 | clientSocket.onMessage((message) => { 15 | console.log('Message from client: ' + message); 16 | clientSocket.sendMessage(message); 17 | }); 18 | 19 | clientSocket.onClosed(() => { 20 | console.log('Client socket closed'); 21 | }); 22 | }); 23 | 24 | // When done, close the server 25 | // ws.stop(); 26 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | clearMocks: true, 5 | collectCoverage: false, 6 | coverageDirectory: 'coverage', 7 | coverageProvider: 'v8', 8 | preset: 'ts-jest', 9 | testEnvironment: 'jest-environment-node', 10 | testRegex: '(/test/jest-tests/.*|(\\.|/)(test|spec))\\.(m)?ts$', 11 | testPathIgnorePatterns: ['node_modules', 'multiple-run.test'], 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-datachannel", 3 | "version": "0.29.0-dev", 4 | "description": "WebRTC For Node.js and Electron. libdatachannel node bindings.", 5 | "main": "./dist/cjs/lib/index.cjs", 6 | "module": "./dist/esm/lib/index.mjs", 7 | "types": "./dist/types/lib/index.d.ts", 8 | "typesVersions": { 9 | "*": { 10 | "*": [ 11 | "dist/types/lib/index.d.ts" 12 | ], 13 | "polyfill": [ 14 | "dist/types/polyfill/index.d.ts" 15 | ] 16 | } 17 | }, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/types/lib/index.d.ts", 21 | "require": "./dist/cjs/lib/index.cjs", 22 | "import": "./dist/esm/lib/index.mjs", 23 | "default": "./dist/lib/esm/index.mjs" 24 | }, 25 | "./polyfill": { 26 | "types": "./dist/types/polyfill/index.d.ts", 27 | "require": "./dist/cjs/polyfill/index.cjs", 28 | "import": "./dist/esm/polyfill/index.mjs", 29 | "default": "./dist/polyfill/esm/index.mjs" 30 | } 31 | }, 32 | "engines": { 33 | "node": ">=18.20.0" 34 | }, 35 | "scripts": { 36 | "install": "prebuild-install -r napi || (npm install --ignore-scripts --production=false && npm run _prebuild)", 37 | "install:nice": "npm run clean && npm install --ignore-scripts --production=false && cmake-js configure --CDUSE_NICE=1 && cmake-js build", 38 | "install:gnu": "npm run clean && npm install --ignore-scripts --production=false && cmake-js configure --CDUSE_GNUTLS=1 && cmake-js build", 39 | "build": "npm run compile && npm run build:tsc", 40 | "compile": "cmake-js build", 41 | "build:debug": "npm run compile:debug && npm run build:tsc", 42 | "compile:debug": "cmake-js configure -D && cmake-js build -D", 43 | "build:tsc": "rimraf dist && rollup -c", 44 | "build:tsc:watch": "rollup -c -w", 45 | "clean": "rimraf dist build", 46 | "lint": "eslint . --ext .ts --ext .mts", 47 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 48 | "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", 49 | "test:wpt": "npm run run:wpt:server & (sleep 8 && (npm run run:wpt:test | tee test/wpt-tests/last-test-results.md) )", 50 | "wpt:server": "cd test/wpt-tests/wpt && ./wpt serve", 51 | "wpt:test": "ts-node test/wpt-tests/index.ts", 52 | "_prebuild": "prebuild -r napi --backend cmake-js", 53 | "prepack": "npm run build:tsc" 54 | }, 55 | "binary": { 56 | "napi_versions": [ 57 | 8 58 | ] 59 | }, 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/murat-dogan/node-datachannel.git" 63 | }, 64 | "keywords": [ 65 | "libdatachannel", 66 | "webrtc", 67 | "p2p", 68 | "peer-to-peer", 69 | "datachannel", 70 | "data channel", 71 | "websocket" 72 | ], 73 | "contributors": [ 74 | { 75 | "name": "Murat Doğan", 76 | "url": "https://github.com/murat-dogan" 77 | }, 78 | { 79 | "name": "Paul-Louis Ageneau", 80 | "url": "https://github.com/paullouisageneau" 81 | } 82 | ], 83 | "license": "MPL 2.0", 84 | "bugs": { 85 | "url": "https://github.com/murat-dogan/node-datachannel/issues" 86 | }, 87 | "homepage": "https://github.com/murat-dogan/node-datachannel#readme", 88 | "devDependencies": { 89 | "@rollup/plugin-esm-shim": "^0.1.7", 90 | "@rollup/plugin-replace": "^6.0.1", 91 | "@types/jest": "^29.5.12", 92 | "@types/node": "^20.6.1", 93 | "@types/webrtc": "^0.0.46", 94 | "@typescript-eslint/eslint-plugin": "^7.17.0", 95 | "@typescript-eslint/parser": "^7.17.0", 96 | "cmake-js": "^7.3.0", 97 | "eslint": "^8.57.0", 98 | "eslint-config-prettier": "^9.1.0", 99 | "eslint-plugin-jest": "^28.6.0", 100 | "eslint-plugin-prettier": "^5.2.1", 101 | "jest": "^29.7.0", 102 | "jsdom": "^24.1.1", 103 | "node-addon-api": "^7.0.0", 104 | "prebuild": "^13.0.1", 105 | "prettier": "^3.3.3", 106 | "puppeteer": "^22.14.0", 107 | "rimraf": "^5.0.9", 108 | "rollup": "^4.22.5", 109 | "rollup-plugin-dts": "^6.1.1", 110 | "rollup-plugin-esbuild": "^6.1.1", 111 | "ts-api-utils": "^1.3.0", 112 | "ts-jest": "^29.2.3", 113 | "ts-node": "^10.9.2", 114 | "typescript": "5.4" 115 | }, 116 | "dependencies": { 117 | "prebuild-install": "^7.1.3" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts'; 2 | import esbuild from 'rollup-plugin-esbuild'; 3 | import esmShim from '@rollup/plugin-esm-shim'; 4 | import replace from '@rollup/plugin-replace'; 5 | 6 | const external = (id) => { 7 | return !/^[./]/.test(id); 8 | }; 9 | 10 | const bundle = (config) => ({ 11 | ...config, 12 | input: 'src/index.ts', 13 | external, 14 | }); 15 | 16 | export default [ 17 | bundle({ 18 | plugins: [ 19 | replace({ 20 | include: 'src/lib/node-datachannel.ts', 21 | preventAssignment: true, 22 | "require('../../build": "require('../../../build", 23 | }), 24 | esmShim(), 25 | esbuild(), 26 | ], 27 | output: [ 28 | { 29 | dir: 'dist/esm', 30 | format: 'es', 31 | exports: 'named', 32 | sourcemap: true, 33 | entryFileNames: '[name].mjs', 34 | preserveModules: true, // Keep directory structure and files 35 | }, 36 | { 37 | dir: 'dist/cjs', 38 | format: 'cjs', 39 | exports: 'named', 40 | sourcemap: true, 41 | entryFileNames: '[name].cjs', 42 | preserveModules: true, // Keep directory structure and files 43 | }, 44 | ], 45 | }), 46 | // types 47 | { 48 | plugins: [dts()], 49 | input: 'src/lib/index.ts', 50 | external, 51 | output: { 52 | dir: 'dist/types/lib', 53 | format: 'cjs', 54 | exports: 'named', 55 | preserveModules: true, // Keep directory structure and files 56 | }, 57 | }, 58 | { 59 | plugins: [dts()], 60 | input: 'src/polyfill/index.ts', 61 | external: (id) => { 62 | if (id.startsWith('../lib')) return true; 63 | return !/^[./]/.test(id); 64 | }, 65 | output: { 66 | dir: 'dist/types/polyfill', 67 | format: 'cjs', 68 | exports: 'named', 69 | preserveModules: true, // Keep directory structure and files 70 | }, 71 | }, 72 | ]; 73 | -------------------------------------------------------------------------------- /src/cpp/data-channel-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef DATA_CHANNEL_WRAPPER_H 2 | #define DATA_CHANNEL_WRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "thread-safe-callback.h" 14 | 15 | class DataChannelWrapper : public Napi::ObjectWrap 16 | { 17 | public: 18 | static Napi::FunctionReference constructor; 19 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 20 | DataChannelWrapper(const Napi::CallbackInfo &info); 21 | ~DataChannelWrapper(); 22 | 23 | // Functions 24 | void close(const Napi::CallbackInfo &info); 25 | Napi::Value getLabel(const Napi::CallbackInfo &info); 26 | Napi::Value getId(const Napi::CallbackInfo &info); 27 | Napi::Value getProtocol(const Napi::CallbackInfo &info); 28 | Napi::Value sendMessage(const Napi::CallbackInfo &info); 29 | Napi::Value sendMessageBinary(const Napi::CallbackInfo &info); 30 | Napi::Value isOpen(const Napi::CallbackInfo &info); 31 | Napi::Value bufferedAmount(const Napi::CallbackInfo &info); 32 | Napi::Value maxMessageSize(const Napi::CallbackInfo &info); 33 | void setBufferedAmountLowThreshold(const Napi::CallbackInfo &info); 34 | 35 | // Callbacks 36 | void onOpen(const Napi::CallbackInfo &info); 37 | void onClosed(const Napi::CallbackInfo &info); 38 | void onError(const Napi::CallbackInfo &info); 39 | void onBufferedAmountLow(const Napi::CallbackInfo &info); 40 | void onMessage(const Napi::CallbackInfo &info); 41 | 42 | // Close all existing DataChannels 43 | static void CloseAll(); 44 | 45 | // Reset all Callbacks for existing DataChannels 46 | static void CleanupAll(); 47 | 48 | private: 49 | static std::unordered_set instances; 50 | 51 | void doClose(); 52 | void doCleanup(); 53 | 54 | std::string mLabel; 55 | std::shared_ptr mDataChannelPtr = nullptr; 56 | 57 | // Callback Ptrs 58 | std::unique_ptr mOnOpenCallback = nullptr; 59 | std::unique_ptr mOnClosedCallback = nullptr; 60 | std::unique_ptr mOnErrorCallback = nullptr; 61 | std::unique_ptr mOnBufferedAmountLowCallback = nullptr; 62 | std::unique_ptr mOnMessageCallback = nullptr; 63 | }; 64 | 65 | #endif // DATA_CHANNEL_WRAPPER_H 66 | -------------------------------------------------------------------------------- /src/cpp/ice-udp-mux-listener-wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "ice-udp-mux-listener-wrapper.h" 2 | 3 | #include "plog/Log.h" 4 | 5 | #include 6 | #include 7 | 8 | Napi::FunctionReference IceUdpMuxListenerWrapper::constructor = Napi::FunctionReference(); 9 | std::unordered_set IceUdpMuxListenerWrapper::instances; 10 | 11 | void IceUdpMuxListenerWrapper::StopAll() 12 | { 13 | PLOG_DEBUG << "IceUdpMuxListenerWrapper StopAll() called"; 14 | auto copy(instances); 15 | for (auto inst : copy) 16 | inst->doCleanup(); 17 | } 18 | 19 | Napi::Object IceUdpMuxListenerWrapper::Init(Napi::Env env, Napi::Object exports) 20 | { 21 | Napi::HandleScope scope(env); 22 | 23 | Napi::Function func = DefineClass( 24 | env, 25 | "IceUdpMuxListener", 26 | { 27 | InstanceMethod("stop", &IceUdpMuxListenerWrapper::stop), 28 | InstanceMethod("onUnhandledStunRequest", &IceUdpMuxListenerWrapper::onUnhandledStunRequest), 29 | InstanceMethod("port", &IceUdpMuxListenerWrapper::port), 30 | InstanceMethod("address", &IceUdpMuxListenerWrapper::address) 31 | }); 32 | 33 | // If this is not the first call, we don't want to reassign the constructor (hot-reload problem) 34 | if(constructor.IsEmpty()) 35 | { 36 | constructor = Napi::Persistent(func); 37 | constructor.SuppressDestruct(); 38 | } 39 | 40 | exports.Set("IceUdpMuxListener", func); 41 | return exports; 42 | } 43 | 44 | IceUdpMuxListenerWrapper::IceUdpMuxListenerWrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap(info) 45 | { 46 | PLOG_DEBUG << "IceUdpMuxListenerWrapper Constructor called"; 47 | Napi::Env env = info.Env(); 48 | int length = info.Length(); 49 | 50 | // We expect (Number, String?) as param 51 | if (length > 0 && info[0].IsNumber()) { 52 | // Port 53 | mPort = info[0].As().ToNumber().Uint32Value(); 54 | } else { 55 | Napi::TypeError::New(env, "Port (Number) and optional Address (String) expected").ThrowAsJavaScriptException(); 56 | return; 57 | } 58 | 59 | if (length > 1 && info[1].IsString()) { 60 | // Address 61 | mAddress = info[1].As().ToString(); 62 | } 63 | 64 | iceUdpMuxListenerPtr = std::make_unique(mPort, mAddress); 65 | instances.insert(this); 66 | } 67 | 68 | IceUdpMuxListenerWrapper::~IceUdpMuxListenerWrapper() 69 | { 70 | PLOG_DEBUG << "IceUdpMuxListenerWrapper Destructor called"; 71 | doCleanup(); 72 | } 73 | 74 | void IceUdpMuxListenerWrapper::doCleanup() 75 | { 76 | PLOG_DEBUG << "IceUdpMuxListenerWrapper::doCleanup() called"; 77 | 78 | if (iceUdpMuxListenerPtr) 79 | { 80 | iceUdpMuxListenerPtr->stop(); 81 | iceUdpMuxListenerPtr.reset(); 82 | } 83 | 84 | mOnUnhandledStunRequestCallback.reset(); 85 | instances.erase(this); 86 | } 87 | 88 | Napi::Value IceUdpMuxListenerWrapper::port(const Napi::CallbackInfo &info) 89 | { 90 | Napi::Env env = info.Env(); 91 | 92 | return Napi::Number::New(env, mPort); 93 | } 94 | 95 | Napi::Value IceUdpMuxListenerWrapper::address(const Napi::CallbackInfo &info) 96 | { 97 | Napi::Env env = info.Env(); 98 | 99 | if (!mAddress.has_value()) { 100 | return env.Undefined(); 101 | } 102 | 103 | return Napi::String::New(env, mAddress.value()); 104 | } 105 | 106 | void IceUdpMuxListenerWrapper::stop(const Napi::CallbackInfo &info) 107 | { 108 | PLOG_DEBUG << "IceUdpMuxListenerWrapper::stop() called"; 109 | doCleanup(); 110 | } 111 | 112 | void IceUdpMuxListenerWrapper::onUnhandledStunRequest(const Napi::CallbackInfo &info) 113 | { 114 | PLOG_DEBUG << "IceUdpMuxListenerWrapper::onUnhandledStunRequest() called"; 115 | Napi::Env env = info.Env(); 116 | int length = info.Length(); 117 | 118 | if (!iceUdpMuxListenerPtr) 119 | { 120 | Napi::Error::New(env, "IceUdpMuxListenerWrapper::onUnhandledStunRequest() called on destroyed IceUdpMuxListener").ThrowAsJavaScriptException(); 121 | return; 122 | } 123 | 124 | if (length < 1 || !info[0].IsFunction()) 125 | { 126 | Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); 127 | return; 128 | } 129 | 130 | // Callback 131 | mOnUnhandledStunRequestCallback = std::make_unique(info[0].As()); 132 | 133 | iceUdpMuxListenerPtr->OnUnhandledStunRequest([&](rtc::IceUdpMuxRequest request) 134 | { 135 | PLOG_DEBUG << "IceUdpMuxListenerWrapper::onUnhandledStunRequest() IceUdpMuxCallback call(1)"; 136 | 137 | if (mOnUnhandledStunRequestCallback) { 138 | mOnUnhandledStunRequestCallback->call([request = std::move(request)](Napi::Env env, std::vector &args) { 139 | Napi::Object reqObj = Napi::Object::New(env); 140 | reqObj.Set("ufrag", request.remoteUfrag.c_str()); 141 | reqObj.Set("host", request.remoteAddress.c_str()); 142 | reqObj.Set("port", request.remotePort); 143 | 144 | args = {reqObj}; 145 | }); 146 | } 147 | 148 | PLOG_DEBUG << "IceUdpMuxListenerWrapper::onUnhandledStunRequest() IceUdpMuxCallback call(2)"; 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /src/cpp/ice-udp-mux-listener-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef ICE_UDP_MUX_LISTENER_WRAPPER_H 2 | #define ICE_UDP_MUX_LISTENER_WRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "thread-safe-callback.h" 9 | 10 | class IceUdpMuxListenerWrapper : public Napi::ObjectWrap 11 | { 12 | public: 13 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 14 | IceUdpMuxListenerWrapper(const Napi::CallbackInfo &info); 15 | ~IceUdpMuxListenerWrapper(); 16 | 17 | // Functions 18 | void stop(const Napi::CallbackInfo &info); 19 | void onUnhandledStunRequest(const Napi::CallbackInfo &info); 20 | 21 | // Stop listening on all ports 22 | static void StopAll(); 23 | 24 | // Properties 25 | Napi::Value port(const Napi::CallbackInfo &info); 26 | Napi::Value address(const Napi::CallbackInfo &info); 27 | Napi::Value unhandledStunRequestCallback(const Napi::CallbackInfo &info); 28 | 29 | // Callback Ptrs 30 | std::unique_ptr mOnUnhandledStunRequestCallback = nullptr; 31 | 32 | private: 33 | static Napi::FunctionReference constructor; 34 | static std::unordered_set instances; 35 | 36 | void doCleanup(); 37 | 38 | std::optional mAddress; 39 | uint16_t mPort; 40 | std::unique_ptr iceUdpMuxListenerPtr = nullptr; 41 | }; 42 | 43 | #endif // ICE_UDP_MUX_LISTENER_WRAPPER_H 44 | -------------------------------------------------------------------------------- /src/cpp/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "rtc-wrapper.h" 3 | #include "peer-connection-wrapper.h" 4 | #include "data-channel-wrapper.h" 5 | #include "ice-udp-mux-listener-wrapper.h" 6 | 7 | #if RTC_ENABLE_MEDIA == 1 8 | #include "media-rtcpreceivingsession-wrapper.h" 9 | #include "media-track-wrapper.h" 10 | #include "media-video-wrapper.h" 11 | #include "media-audio-wrapper.h" 12 | #endif 13 | 14 | #if RTC_ENABLE_WEBSOCKET == 1 15 | #include "web-socket-wrapper.h" 16 | #include "web-socket-server-wrapper.h" 17 | #endif 18 | 19 | Napi::Object InitAll(Napi::Env env, Napi::Object exports) 20 | { 21 | RtcWrapper::Init(env, exports); 22 | 23 | #if RTC_ENABLE_MEDIA == 1 24 | RtcpReceivingSessionWrapper::Init(env, exports); 25 | TrackWrapper::Init(env, exports); 26 | VideoWrapper::Init(env, exports); 27 | AudioWrapper::Init(env, exports); 28 | #endif 29 | 30 | DataChannelWrapper::Init(env, exports); 31 | IceUdpMuxListenerWrapper::Init(env, exports); 32 | PeerConnectionWrapper::Init(env, exports); 33 | 34 | #if RTC_ENABLE_WEBSOCKET == 1 35 | WebSocketWrapper::Init(env, exports); 36 | WebSocketServerWrapper::Init(env, exports); 37 | #endif 38 | 39 | return exports; 40 | } 41 | 42 | NODE_API_MODULE(nodeDataChannel, InitAll) 43 | -------------------------------------------------------------------------------- /src/cpp/media-audio-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef MEDIA_AUDIO_WRAPPER_H 2 | #define MEDIA_AUDIO_WRAPPER_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "thread-safe-callback.h" 10 | 11 | class AudioWrapper : public Napi::ObjectWrap 12 | { 13 | public: 14 | static Napi::FunctionReference constructor; 15 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 16 | AudioWrapper(const Napi::CallbackInfo &info); 17 | ~AudioWrapper(); 18 | 19 | rtc::Description::Audio getAudioInstance(); 20 | 21 | // Functions 22 | void addAudioCodec(const Napi::CallbackInfo &info); 23 | void addOpusCodec(const Napi::CallbackInfo &info); 24 | 25 | // class Entry 26 | Napi::Value direction(const Napi::CallbackInfo &info); 27 | Napi::Value generateSdp(const Napi::CallbackInfo &info); 28 | Napi::Value mid(const Napi::CallbackInfo &info); 29 | void setDirection(const Napi::CallbackInfo &info); 30 | 31 | // class Media 32 | Napi::Value description(const Napi::CallbackInfo &info); 33 | void removeFormat(const Napi::CallbackInfo &info); 34 | void addSSRC(const Napi::CallbackInfo &info); 35 | void removeSSRC(const Napi::CallbackInfo &info); 36 | void replaceSSRC(const Napi::CallbackInfo &info); 37 | Napi::Value hasSSRC(const Napi::CallbackInfo &info); 38 | Napi::Value getSSRCs(const Napi::CallbackInfo &info); 39 | Napi::Value getCNameForSsrc(const Napi::CallbackInfo &info); 40 | void setBitrate(const Napi::CallbackInfo &info); 41 | Napi::Value getBitrate(const Napi::CallbackInfo &info); 42 | Napi::Value hasPayloadType(const Napi::CallbackInfo &info); 43 | void addRTXCodec(const Napi::CallbackInfo &info); 44 | void addRTPMap(const Napi::CallbackInfo &info); 45 | void parseSdpLine(const Napi::CallbackInfo &info); 46 | 47 | // Callbacks 48 | 49 | private: 50 | static std::unordered_set instances; 51 | std::shared_ptr mAudioPtr = nullptr; 52 | }; 53 | 54 | #endif // MEDIA_AUDIO_WRAPPER_H -------------------------------------------------------------------------------- /src/cpp/media-direction.cpp: -------------------------------------------------------------------------------- 1 | #include "media-direction.h" 2 | 3 | rtc::Description::Direction strToDirection(const std::string dirAsStr) 4 | { 5 | rtc::Description::Direction dir = rtc::Description::Direction::Unknown; 6 | 7 | if (dirAsStr == "SendOnly") 8 | dir = rtc::Description::Direction::SendOnly; 9 | if (dirAsStr == "SendRecv") 10 | dir = rtc::Description::Direction::SendRecv; 11 | if (dirAsStr == "RecvOnly") 12 | dir = rtc::Description::Direction::RecvOnly; 13 | if (dirAsStr == "Inactive") 14 | dir = rtc::Description::Direction::Inactive; 15 | 16 | return dir; 17 | } 18 | 19 | std::string directionToStr(rtc::Description::Direction dir) 20 | { 21 | std::string dirAsStr; 22 | switch (dir) 23 | { 24 | case rtc::Description::Direction::Unknown: 25 | dirAsStr = "Unknown"; 26 | break; 27 | case rtc::Description::Direction::SendOnly: 28 | dirAsStr = "SendOnly"; 29 | break; 30 | case rtc::Description::Direction::RecvOnly: 31 | dirAsStr = "RecvOnly"; 32 | break; 33 | case rtc::Description::Direction::SendRecv: 34 | dirAsStr = "SendRecv"; 35 | break; 36 | case rtc::Description::Direction::Inactive: 37 | dirAsStr = "Inactive"; 38 | break; 39 | default: 40 | dirAsStr = "UNKNOWN_DIR_TYPE"; 41 | } 42 | return dirAsStr; 43 | } 44 | -------------------------------------------------------------------------------- /src/cpp/media-direction.h: -------------------------------------------------------------------------------- 1 | #ifndef MEDIA_DIRECTION_H 2 | #define MEDIA_DIRECTION_H 3 | 4 | #include 5 | #include 6 | 7 | rtc::Description::Direction strToDirection(const std::string dirAsStr); 8 | std::string directionToStr(rtc::Description::Direction dir); 9 | 10 | #endif // MEDIA_DIRECTION_H 11 | -------------------------------------------------------------------------------- /src/cpp/media-rtcpreceivingsession-wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "media-rtcpreceivingsession-wrapper.h" 2 | 3 | Napi::FunctionReference RtcpReceivingSessionWrapper::constructor = Napi::FunctionReference(); 4 | std::unordered_set RtcpReceivingSessionWrapper::instances; 5 | 6 | Napi::Object RtcpReceivingSessionWrapper::Init(Napi::Env env, Napi::Object exports) 7 | { 8 | Napi::HandleScope scope(env); 9 | 10 | Napi::Function func = Napi::ObjectWrap::DefineClass( 11 | env, 12 | "RtcpReceivingSession", 13 | { 14 | // Instance Methods 15 | }); 16 | 17 | // If this is not the first call, we don't want to reassign the constructor (hot-reload problem) 18 | if(constructor.IsEmpty()) 19 | { 20 | constructor = Napi::Persistent(func); 21 | constructor.SuppressDestruct(); 22 | } 23 | 24 | exports.Set("RtcpReceivingSession", func); 25 | return exports; 26 | } 27 | 28 | RtcpReceivingSessionWrapper::RtcpReceivingSessionWrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap(info) 29 | { 30 | Napi::Env env = info.Env(); 31 | mSessionPtr = std::make_unique(); 32 | instances.insert(this); 33 | } 34 | 35 | RtcpReceivingSessionWrapper::~RtcpReceivingSessionWrapper() 36 | { 37 | mSessionPtr.reset(); 38 | instances.erase(this); 39 | } 40 | 41 | std::shared_ptr RtcpReceivingSessionWrapper::getSessionInstance() 42 | { 43 | return mSessionPtr; 44 | } 45 | -------------------------------------------------------------------------------- /src/cpp/media-rtcpreceivingsession-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef MEDIA_RTCPRECEIVINGSESSION_WRAPPER_H 2 | #define MEDIA_RTCPRECEIVINGSESSION_WRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "thread-safe-callback.h" 14 | 15 | class RtcpReceivingSessionWrapper : public Napi::ObjectWrap 16 | { 17 | public: 18 | static Napi::FunctionReference constructor; 19 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 20 | RtcpReceivingSessionWrapper(const Napi::CallbackInfo &info); 21 | ~RtcpReceivingSessionWrapper(); 22 | std::shared_ptr getSessionInstance(); 23 | 24 | // Functions 25 | 26 | // Callbacks 27 | 28 | private: 29 | static std::unordered_set instances; 30 | std::shared_ptr mSessionPtr = nullptr; 31 | }; 32 | 33 | #endif // MEDIA_RTCPRECEIVINGSESSION_WRAPPER_H 34 | -------------------------------------------------------------------------------- /src/cpp/media-track-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef MEDIA_TRACK_WRAPPER_H 2 | #define MEDIA_TRACK_WRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "thread-safe-callback.h" 14 | 15 | class TrackWrapper : public Napi::ObjectWrap 16 | { 17 | public: 18 | static Napi::FunctionReference constructor; 19 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 20 | TrackWrapper(const Napi::CallbackInfo &info); 21 | ~TrackWrapper(); 22 | 23 | // Functions 24 | Napi::Value direction(const Napi::CallbackInfo &info); 25 | Napi::Value mid(const Napi::CallbackInfo &info); 26 | Napi::Value type(const Napi::CallbackInfo &info); 27 | void close(const Napi::CallbackInfo &info); 28 | Napi::Value sendMessage(const Napi::CallbackInfo &info); 29 | Napi::Value sendMessageBinary(const Napi::CallbackInfo &info); 30 | Napi::Value isOpen(const Napi::CallbackInfo &info); 31 | Napi::Value isClosed(const Napi::CallbackInfo &info); 32 | Napi::Value maxMessageSize(const Napi::CallbackInfo &info); 33 | Napi::Value requestBitrate(const Napi::CallbackInfo &info); 34 | Napi::Value requestKeyframe(const Napi::CallbackInfo &info); 35 | void setMediaHandler(const Napi::CallbackInfo &info); 36 | 37 | // Callbacks 38 | void onOpen(const Napi::CallbackInfo &info); 39 | void onClosed(const Napi::CallbackInfo &info); 40 | void onError(const Napi::CallbackInfo &info); 41 | void onMessage(const Napi::CallbackInfo &info); 42 | 43 | // Close all existing tracks 44 | static void CloseAll(); 45 | 46 | // Reset all Callbacks for existing tracks 47 | static void CleanupAll(); 48 | 49 | private: 50 | static std::unordered_set instances; 51 | 52 | void doClose(); 53 | void doCleanup(); 54 | 55 | std::shared_ptr mTrackPtr = nullptr; 56 | 57 | // Callback Ptrs 58 | std::unique_ptr mOnOpenCallback = nullptr; 59 | std::unique_ptr mOnClosedCallback = nullptr; 60 | std::unique_ptr mOnErrorCallback = nullptr; 61 | std::unique_ptr mOnMessageCallback = nullptr; 62 | }; 63 | 64 | #endif // MEDIA_TRACK_WRAPPER_H 65 | -------------------------------------------------------------------------------- /src/cpp/media-video-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef MEDIA_VIDEO_WRAPPER_H 2 | #define MEDIA_VIDEO_WRAPPER_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "thread-safe-callback.h" 10 | 11 | class VideoWrapper : public Napi::ObjectWrap 12 | { 13 | public: 14 | static Napi::FunctionReference constructor; 15 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 16 | VideoWrapper(const Napi::CallbackInfo &info); 17 | ~VideoWrapper(); 18 | 19 | rtc::Description::Video getVideoInstance(); 20 | 21 | // Functions 22 | void addVideoCodec(const Napi::CallbackInfo &info); 23 | void addH264Codec(const Napi::CallbackInfo &info); 24 | void addVP8Codec(const Napi::CallbackInfo &info); 25 | void addVP9Codec(const Napi::CallbackInfo &info); 26 | 27 | // class Entry 28 | Napi::Value direction(const Napi::CallbackInfo &info); 29 | Napi::Value generateSdp(const Napi::CallbackInfo &info); 30 | Napi::Value mid(const Napi::CallbackInfo &info); 31 | void setDirection(const Napi::CallbackInfo &info); 32 | 33 | // class Media 34 | Napi::Value description(const Napi::CallbackInfo &info); 35 | void removeFormat(const Napi::CallbackInfo &info); 36 | void addSSRC(const Napi::CallbackInfo &info); 37 | void removeSSRC(const Napi::CallbackInfo &info); 38 | void replaceSSRC(const Napi::CallbackInfo &info); 39 | Napi::Value hasSSRC(const Napi::CallbackInfo &info); 40 | Napi::Value getSSRCs(const Napi::CallbackInfo &info); 41 | Napi::Value getCNameForSsrc(const Napi::CallbackInfo &info); 42 | void setBitrate(const Napi::CallbackInfo &info); 43 | Napi::Value getBitrate(const Napi::CallbackInfo &info); 44 | Napi::Value hasPayloadType(const Napi::CallbackInfo &info); 45 | void addRTXCodec(const Napi::CallbackInfo &info); 46 | void addRTPMap(const Napi::CallbackInfo &info); 47 | void parseSdpLine(const Napi::CallbackInfo &info); 48 | 49 | // Callbacks 50 | 51 | private: 52 | static std::unordered_set instances; 53 | std::shared_ptr mVideoPtr = nullptr; 54 | }; 55 | 56 | #endif // MEDIA_VIDEO_WRAPPER_H -------------------------------------------------------------------------------- /src/cpp/peer-connection-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef PEER_CONNECTION_WRAPPER_H 2 | #define PEER_CONNECTION_WRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "thread-safe-callback.h" 14 | 15 | class PeerConnectionWrapper : public Napi::ObjectWrap 16 | { 17 | public: 18 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 19 | PeerConnectionWrapper(const Napi::CallbackInfo &info); 20 | ~PeerConnectionWrapper(); 21 | 22 | // Functions 23 | void close(const Napi::CallbackInfo &info); 24 | void setLocalDescription(const Napi::CallbackInfo &info); 25 | void setRemoteDescription(const Napi::CallbackInfo &info); 26 | Napi::Value localDescription(const Napi::CallbackInfo &info); 27 | Napi::Value remoteDescription(const Napi::CallbackInfo &info); 28 | void addRemoteCandidate(const Napi::CallbackInfo &info); 29 | Napi::Value createDataChannel(const Napi::CallbackInfo &info); 30 | 31 | #if RTC_ENABLE_MEDIA == 1 32 | Napi::Value addTrack(const Napi::CallbackInfo &info); 33 | void onTrack(const Napi::CallbackInfo &info); 34 | #endif 35 | 36 | Napi::Value hasMedia(const Napi::CallbackInfo &info); 37 | Napi::Value state(const Napi::CallbackInfo &info); 38 | Napi::Value iceState(const Napi::CallbackInfo &info); 39 | Napi::Value signalingState(const Napi::CallbackInfo &info); 40 | Napi::Value gatheringState(const Napi::CallbackInfo &info); 41 | Napi::Value remoteFingerprint(const Napi::CallbackInfo &info); 42 | 43 | // Callbacks 44 | void onLocalDescription(const Napi::CallbackInfo &info); 45 | void onLocalCandidate(const Napi::CallbackInfo &info); 46 | void onStateChange(const Napi::CallbackInfo &info); 47 | void onIceStateChange(const Napi::CallbackInfo &info); 48 | void onSignalingStateChange(const Napi::CallbackInfo &info); 49 | void onGatheringStateChange(const Napi::CallbackInfo &info); 50 | void onDataChannel(const Napi::CallbackInfo &info); 51 | 52 | // Stats 53 | Napi::Value bytesSent(const Napi::CallbackInfo &info); 54 | Napi::Value bytesReceived(const Napi::CallbackInfo &info); 55 | Napi::Value rtt(const Napi::CallbackInfo &info); 56 | Napi::Value getSelectedCandidatePair(const Napi::CallbackInfo &info); 57 | Napi::Value maxDataChannelId(const Napi::CallbackInfo &info); 58 | Napi::Value maxMessageSize(const Napi::CallbackInfo &info); 59 | 60 | // Close all existing Peer Connections 61 | static void CloseAll(); 62 | 63 | // Reset all Callbacks for existing Peer Connections 64 | static void CleanupAll(); 65 | 66 | private: 67 | static Napi::FunctionReference constructor; 68 | static std::unordered_set instances; 69 | 70 | void doClose(); 71 | void doCleanup(); 72 | 73 | std::string mPeerName; 74 | std::unique_ptr mRtcPeerConnPtr = nullptr; 75 | 76 | // Callback Ptrs 77 | std::unique_ptr mOnLocalDescriptionCallback = nullptr; 78 | std::unique_ptr mOnLocalCandidateCallback = nullptr; 79 | std::unique_ptr mOnStateChangeCallback = nullptr; 80 | std::unique_ptr mOnIceStateChangeCallback = nullptr; 81 | std::unique_ptr mOnSignalingStateChangeCallback = nullptr; 82 | std::unique_ptr mOnGatheringStateChangeCallback = nullptr; 83 | std::unique_ptr mOnDataChannelCallback = nullptr; 84 | std::unique_ptr mOnTrackCallback = nullptr; 85 | 86 | // Helpers 87 | std::string candidateTypeToString(const rtc::Candidate::Type &type); 88 | std::string candidateTransportTypeToString(const rtc::Candidate::TransportType &transportType); 89 | }; 90 | 91 | #endif // PEER_CONNECTION_WRAPPER_H 92 | -------------------------------------------------------------------------------- /src/cpp/rtc-wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "rtc-wrapper.h" 2 | #include "peer-connection-wrapper.h" 3 | #include "data-channel-wrapper.h" 4 | 5 | #if RTC_ENABLE_MEDIA == 1 6 | #include "media-track-wrapper.h" 7 | #endif 8 | 9 | #if RTC_ENABLE_WEBSOCKET == 1 10 | #include "web-socket-wrapper.h" 11 | #include "web-socket-server-wrapper.h" 12 | #endif 13 | 14 | #include "plog/Log.h" 15 | 16 | #include 17 | #include 18 | 19 | Napi::Object RtcWrapper::Init(Napi::Env env, Napi::Object exports) 20 | { 21 | Napi::HandleScope scope(env); 22 | 23 | exports.Set("initLogger", Napi::Function::New(env, &RtcWrapper::initLogger)); 24 | exports.Set("cleanup", Napi::Function::New(env, &RtcWrapper::cleanup)); 25 | exports.Set("preload", Napi::Function::New(env, &RtcWrapper::preload)); 26 | exports.Set("setSctpSettings", Napi::Function::New(env, &RtcWrapper::setSctpSettings)); 27 | exports.Set("getLibraryVersion", Napi::Function::New(env, &RtcWrapper::getLibraryVersion)); 28 | 29 | return exports; 30 | } 31 | 32 | void RtcWrapper::preload(const Napi::CallbackInfo &info) 33 | { 34 | PLOG_DEBUG << "preload() called"; 35 | Napi::Env env = info.Env(); 36 | try 37 | { 38 | rtc::Preload(); 39 | } 40 | catch (std::exception &ex) 41 | { 42 | Napi::Error::New(env, std::string("libdatachannel error# ") + ex.what()).ThrowAsJavaScriptException(); 43 | } 44 | } 45 | 46 | void RtcWrapper::initLogger(const Napi::CallbackInfo &info) 47 | { 48 | Napi::Env env = info.Env(); 49 | int length = info.Length(); 50 | 51 | // We expect (String, Object, Function) as param 52 | if (length < 1 || !info[0].IsString()) 53 | { 54 | Napi::TypeError::New(env, "LogLevel(String) expected").ThrowAsJavaScriptException(); 55 | return; 56 | } 57 | 58 | std::string logLevelStr = info[0].As().ToString(); 59 | rtc::LogLevel logLevel = rtc::LogLevel::None; 60 | 61 | if (logLevelStr == "Verbose") 62 | logLevel = rtc::LogLevel::Verbose; 63 | if (logLevelStr == "Debug") 64 | logLevel = rtc::LogLevel::Debug; 65 | if (logLevelStr == "Info") 66 | logLevel = rtc::LogLevel::Info; 67 | if (logLevelStr == "Warning") 68 | logLevel = rtc::LogLevel::Warning; 69 | if (logLevelStr == "Error") 70 | logLevel = rtc::LogLevel::Error; 71 | if (logLevelStr == "Fatal") 72 | logLevel = rtc::LogLevel::Fatal; 73 | 74 | try 75 | { 76 | if (length < 2) 77 | { 78 | rtc::InitLogger(logLevel); 79 | } 80 | else 81 | { 82 | if (!info[1].IsFunction()) 83 | { 84 | Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); 85 | return; 86 | } 87 | logCallback = std::make_unique(info[1].As()); 88 | rtc::InitLogger(logLevel, [&](rtc::LogLevel level, std::string message) 89 | { 90 | if (logCallback) 91 | logCallback->call([level, message = std::move(message)](Napi::Env env, std::vector &args) { 92 | // This will run in main thread and needs to construct the 93 | // arguments for the call 94 | 95 | std::string logLevel; 96 | if (level == rtc::LogLevel::Verbose) 97 | logLevel = "Verbose"; 98 | if (level == rtc::LogLevel::Debug) 99 | logLevel = "Debug"; 100 | if (level == rtc::LogLevel::Info) 101 | logLevel = "Info"; 102 | if (level == rtc::LogLevel::Warning) 103 | logLevel = "Warning"; 104 | if (level == rtc::LogLevel::Error) 105 | logLevel = "Error"; 106 | if (level == rtc::LogLevel::Fatal) 107 | logLevel = "Fatal"; 108 | args = {Napi::String::New(env, logLevel), Napi::String::New(env, message)}; 109 | }); }); 110 | } 111 | } 112 | catch (std::exception &ex) 113 | { 114 | Napi::Error::New(env, std::string("libdatachannel error# ") + ex.what()).ThrowAsJavaScriptException(); 115 | return; 116 | } 117 | } 118 | 119 | void RtcWrapper::cleanup(const Napi::CallbackInfo &info) 120 | { 121 | PLOG_DEBUG << "cleanup() called"; 122 | Napi::Env env = info.Env(); 123 | try 124 | { 125 | PeerConnectionWrapper::CloseAll(); 126 | DataChannelWrapper::CloseAll(); 127 | 128 | #if RTC_ENABLE_MEDIA == 1 129 | TrackWrapper::CloseAll(); 130 | #endif 131 | 132 | #if RTC_ENABLE_WEBSOCKET == 1 133 | WebSocketWrapper::CloseAll(); 134 | WebSocketServerWrapper::StopAll(); 135 | #endif 136 | 137 | const auto timeout = std::chrono::seconds(10); 138 | if (rtc::Cleanup().wait_for(std::chrono::seconds(timeout)) == std::future_status::timeout) 139 | throw std::runtime_error("cleanup timeout (possible deadlock)"); 140 | 141 | // Cleanup the instances 142 | PeerConnectionWrapper::CleanupAll(); 143 | DataChannelWrapper::CleanupAll(); 144 | 145 | #if RTC_ENABLE_MEDIA == 1 146 | TrackWrapper::CleanupAll(); 147 | #endif 148 | 149 | #if RTC_ENABLE_WEBSOCKET == 1 150 | WebSocketWrapper::CleanupAll(); 151 | #endif 152 | 153 | if (logCallback) 154 | logCallback.reset(); 155 | } 156 | catch (std::exception &ex) 157 | { 158 | Napi::Error::New(env, std::string("libdatachannel error# ") + ex.what()).ThrowAsJavaScriptException(); 159 | return; 160 | } 161 | } 162 | 163 | void RtcWrapper::setSctpSettings(const Napi::CallbackInfo &info) 164 | { 165 | PLOG_DEBUG << "setSctpSettings() called"; 166 | Napi::Env env = info.Env(); 167 | int length = info.Length(); 168 | 169 | // We expect (Object) as param 170 | if (length < 1 || !info[0].IsObject()) 171 | { 172 | Napi::TypeError::New(env, "Configuration (Object) expected").ThrowAsJavaScriptException(); 173 | return; 174 | } 175 | 176 | rtc::SctpSettings settings; 177 | Napi::Object config = info[0].As(); 178 | 179 | if (config.Get("recvBufferSize").IsNumber()) 180 | settings.recvBufferSize = config.Get("recvBufferSize").As().Uint32Value(); 181 | if (config.Get("sendBufferSize").IsNumber()) 182 | settings.sendBufferSize = config.Get("sendBufferSize").As().Uint32Value(); 183 | if (config.Get("maxChunksOnQueue").IsNumber()) 184 | settings.maxChunksOnQueue = config.Get("maxChunksOnQueue").As().Uint32Value(); 185 | if (config.Get("initialCongestionWindow").IsNumber()) 186 | settings.initialCongestionWindow = config.Get("initialCongestionWindow").As().Uint32Value(); 187 | if (config.Get("congestionControlModule").IsNumber()) 188 | settings.congestionControlModule = config.Get("congestionControlModule").As().Uint32Value(); 189 | if (config.Get("delayedSackTime").IsNumber()) 190 | settings.delayedSackTime = std::chrono::milliseconds(config.Get("delayedSackTime").As().Uint32Value()); 191 | 192 | rtc::SetSctpSettings(settings); 193 | } 194 | 195 | 196 | Napi::Value RtcWrapper::getLibraryVersion(const Napi::CallbackInfo &info) 197 | { 198 | PLOG_DEBUG << "getLibraryVersion() called"; 199 | Napi::Env env = info.Env(); 200 | return Napi::String::New(info.Env(), RTC_VERSION); 201 | } 202 | -------------------------------------------------------------------------------- /src/cpp/rtc-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef RTC_WRAPPER_H 2 | #define RTC_WRAPPER_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "rtc/rtc.hpp" 10 | 11 | #include "thread-safe-callback.h" 12 | 13 | class RtcWrapper 14 | { 15 | public: 16 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 17 | static void preload(const Napi::CallbackInfo &info); 18 | static void initLogger(const Napi::CallbackInfo &info); 19 | static void cleanup(const Napi::CallbackInfo &info); 20 | static void setSctpSettings(const Napi::CallbackInfo &info); 21 | static Napi::Value getLibraryVersion(const Napi::CallbackInfo &info); 22 | private: 23 | static inline std::unique_ptr logCallback = nullptr; 24 | }; 25 | 26 | #endif // RTC_WRAPPER_H 27 | -------------------------------------------------------------------------------- /src/cpp/thread-safe-callback.cpp: -------------------------------------------------------------------------------- 1 | #include "thread-safe-callback.h" 2 | 3 | #include 4 | 5 | const char *ThreadSafeCallback::CancelException::what() const throw() 6 | { 7 | return "ThreadSafeCallback cancelled"; 8 | } 9 | 10 | ThreadSafeCallback::ThreadSafeCallback(Napi::Function callback) 11 | { 12 | Napi::Env env = callback.Env(); 13 | 14 | if (!callback.IsFunction()) 15 | throw Napi::Error::New(env, "Callback must be a function"); 16 | 17 | tsfn = tsfn_t::New(env, 18 | std::move(callback), 19 | "ThreadSafeCallback callback", 20 | 0, // unlimited queue 21 | 1); 22 | } 23 | 24 | ThreadSafeCallback::~ThreadSafeCallback() 25 | { 26 | tsfn.Abort(); 27 | } 28 | 29 | void ThreadSafeCallback::call(arg_func_t argFunc, cleanup_func_t cleanupFunc) 30 | { 31 | CallbackData *data = new CallbackData{std::move(argFunc), std::move(cleanupFunc)}; 32 | if (tsfn.BlockingCall(data) != napi_ok) 33 | { 34 | delete data; 35 | throw std::runtime_error("Failed to call JavaScript callback"); 36 | } 37 | } 38 | 39 | void ThreadSafeCallback::callbackFunc(Napi::Env env, 40 | Napi::Function callback, 41 | ContextType *context, 42 | CallbackData *data) 43 | { 44 | // if env is gone, it could mean this cb was destroyed. See issue#176 45 | if (!data || !env) 46 | return; 47 | 48 | arg_vector_t args; 49 | arg_func_t argFunc(std::move(data->argFunc)); 50 | cleanup_func_t cleanup(std::move(data->cleanupFunc)); 51 | delete data; 52 | 53 | try 54 | { 55 | argFunc(env, args); 56 | } 57 | catch (CancelException &) 58 | { 59 | return; 60 | } 61 | 62 | if (callback) 63 | { 64 | callback.Call(args); 65 | } 66 | 67 | cleanup(); 68 | } 69 | -------------------------------------------------------------------------------- /src/cpp/thread-safe-callback.h: -------------------------------------------------------------------------------- 1 | #ifndef THREAD_SAFE_CALLBACK_H 2 | #define THREAD_SAFE_CALLBACK_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | class ThreadSafeCallback 10 | { 11 | public: 12 | using arg_vector_t = std::vector; 13 | using arg_func_t = std::function; 14 | using cleanup_func_t = std::function; 15 | 16 | ThreadSafeCallback(Napi::Function callback); 17 | ~ThreadSafeCallback(); 18 | 19 | ThreadSafeCallback(const ThreadSafeCallback &) = delete; 20 | ThreadSafeCallback(ThreadSafeCallback &&) = delete; 21 | 22 | ThreadSafeCallback &operator=(const ThreadSafeCallback &) = delete; 23 | ThreadSafeCallback &operator=(ThreadSafeCallback &&) = delete; 24 | 25 | void call(arg_func_t argFunc, cleanup_func_t cleanupFunc = []() {}); 26 | 27 | class CancelException : public std::exception 28 | { 29 | const char *what() const throw(); 30 | }; 31 | 32 | private: 33 | using ContextType = std::nullptr_t; 34 | struct CallbackData 35 | { 36 | arg_func_t argFunc; 37 | cleanup_func_t cleanupFunc; 38 | }; 39 | 40 | static void callbackFunc(Napi::Env env, 41 | Napi::Function callback, 42 | ContextType *context, 43 | CallbackData *data); 44 | 45 | using tsfn_t = Napi::TypedThreadSafeFunction; 46 | tsfn_t tsfn; 47 | }; 48 | 49 | #endif // THREAD_SAFE_CALLBACK_H 50 | -------------------------------------------------------------------------------- /src/cpp/web-socket-server-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef WEB_SOCKET_SERVER_WRAPPER_H 2 | #define WEB_SOCKET_SERVER_WRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "web-socket-wrapper.h" 14 | #include "thread-safe-callback.h" 15 | 16 | class WebSocketServerWrapper : public Napi::ObjectWrap 17 | { 18 | public: 19 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 20 | WebSocketServerWrapper(const Napi::CallbackInfo &info); 21 | ~WebSocketServerWrapper(); 22 | 23 | // Functions 24 | void stop(const Napi::CallbackInfo &info); 25 | Napi::Value port(const Napi::CallbackInfo &info); 26 | 27 | // Callbacks 28 | void onClient(const Napi::CallbackInfo &info); 29 | 30 | // Close all existing WebSocketServers 31 | static void StopAll(); 32 | 33 | private: 34 | static Napi::FunctionReference constructor; 35 | static std::unordered_set instances; 36 | 37 | void doStop(); 38 | 39 | std::unique_ptr mWebSocketServerPtr = nullptr; 40 | 41 | // Callback Ptrs 42 | std::unique_ptr mOnClientCallback = nullptr; 43 | }; 44 | 45 | #endif // WEB_SOCKET_SERVER_WRAPPER_H 46 | -------------------------------------------------------------------------------- /src/cpp/web-socket-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef WEB_SOCKET_WRAPPER_H 2 | #define WEB_SOCKET_WRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "thread-safe-callback.h" 14 | 15 | class WebSocketWrapper : public Napi::ObjectWrap 16 | { 17 | public: 18 | static Napi::FunctionReference constructor; 19 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 20 | WebSocketWrapper(const Napi::CallbackInfo &info); 21 | ~WebSocketWrapper(); 22 | 23 | // Functions 24 | void open(const Napi::CallbackInfo &info); 25 | void close(const Napi::CallbackInfo &info); 26 | void forceClose(const Napi::CallbackInfo &info); 27 | Napi::Value sendMessage(const Napi::CallbackInfo &info); 28 | Napi::Value sendMessageBinary(const Napi::CallbackInfo &info); 29 | Napi::Value isOpen(const Napi::CallbackInfo &info); 30 | Napi::Value bufferedAmount(const Napi::CallbackInfo &info); 31 | Napi::Value maxMessageSize(const Napi::CallbackInfo &info); 32 | void setBufferedAmountLowThreshold(const Napi::CallbackInfo &info); 33 | Napi::Value remoteAddress(const Napi::CallbackInfo &info); 34 | Napi::Value path(const Napi::CallbackInfo &info); 35 | 36 | // Callbacks 37 | void onOpen(const Napi::CallbackInfo &info); 38 | void onClosed(const Napi::CallbackInfo &info); 39 | void onError(const Napi::CallbackInfo &info); 40 | void onBufferedAmountLow(const Napi::CallbackInfo &info); 41 | void onMessage(const Napi::CallbackInfo &info); 42 | 43 | // Close all existing WebSockets 44 | static void CloseAll(); 45 | 46 | // Reset all Callbacks for existing WebSockets 47 | static void CleanupAll(); 48 | 49 | private: 50 | static std::unordered_set instances; 51 | 52 | void doClose(); 53 | void doForceClose(); 54 | void doCleanup(); 55 | 56 | std::shared_ptr mWebSocketPtr = nullptr; 57 | 58 | // Callback Ptrs 59 | std::unique_ptr mOnOpenCallback = nullptr; 60 | std::unique_ptr mOnClosedCallback = nullptr; 61 | std::unique_ptr mOnErrorCallback = nullptr; 62 | std::unique_ptr mOnBufferedAmountLowCallback = nullptr; 63 | std::unique_ptr mOnMessageCallback = nullptr; 64 | }; 65 | 66 | #endif // WEB_SOCKET_WRAPPER_H 67 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // This file is created for builder process 2 | // So builder can find and compile all files and folders 3 | // Not intended to be imported directly 4 | 5 | import n from './lib/index'; 6 | import p from './polyfill/index'; 7 | 8 | export const nodeDataChannel = n; 9 | export const polyfill = p; 10 | -------------------------------------------------------------------------------- /src/lib/datachannel-stream.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as stream from 'stream'; 3 | 4 | /** 5 | * Turns a node-datachannel DataChannel into a real Node.js stream, complete with buffering, 6 | * backpressure (up to a point - if the buffer fills up, messages are dropped), and 7 | * support for piping data elsewhere. 8 | * 9 | * Read & written data may be either UTF-8 strings or Buffers - this difference exists at 10 | * the protocol level, and is preserved here throughout. 11 | */ 12 | export default class DataChannelStream extends stream.Duplex { 13 | private _rawChannel: any; 14 | private _readActive: boolean; 15 | 16 | constructor(rawChannel: any, streamOptions?: Omit) { 17 | super({ 18 | allowHalfOpen: false, // Default to autoclose on end(). 19 | ...streamOptions, 20 | objectMode: true, // Preserve the string/buffer distinction (WebRTC treats them differently) 21 | }); 22 | 23 | this._rawChannel = rawChannel; 24 | this._readActive = true; 25 | 26 | rawChannel.onMessage((msg: any) => { 27 | if (!this._readActive) return; // If the buffer is full, drop messages. 28 | 29 | // If the push is rejected, we pause reading until the next call to _read(). 30 | this._readActive = this.push(msg); 31 | }); 32 | 33 | // When the DataChannel closes, the readable & writable ends close 34 | rawChannel.onClosed(() => { 35 | this.push(null); 36 | this.destroy(); 37 | }); 38 | 39 | rawChannel.onError((errMsg: string) => { 40 | this.destroy(new Error(`DataChannel error: ${errMsg}`)); 41 | }); 42 | 43 | // Buffer all writes until the DataChannel opens 44 | if (!rawChannel.isOpen()) { 45 | this.cork(); 46 | rawChannel.onOpen(() => this.uncork()); 47 | } 48 | } 49 | 50 | _read(): void { 51 | // Stop dropping messages, if the buffer filling up meant we were doing so before. 52 | this._readActive = true; 53 | } 54 | 55 | _write(chunk, _encoding, callback): void { 56 | let sentOk; 57 | 58 | try { 59 | if (Buffer.isBuffer(chunk)) { 60 | sentOk = this._rawChannel.sendMessageBinary(chunk); 61 | } else if (typeof chunk === 'string') { 62 | sentOk = this._rawChannel.sendMessage(chunk); 63 | } else { 64 | const typeName = chunk.constructor.name || typeof chunk; 65 | throw new Error(`Cannot write ${typeName} to DataChannel stream`); 66 | } 67 | } catch (err) { 68 | return callback(err); 69 | } 70 | 71 | if (sentOk) { 72 | callback(null); 73 | } else { 74 | callback(new Error('Failed to write to DataChannel')); 75 | } 76 | } 77 | 78 | _final(callback): void { 79 | if (!this.allowHalfOpen) this.destroy(); 80 | callback(null); 81 | } 82 | 83 | _destroy(maybeErr, callback): void { 84 | // When the stream is destroyed, we close the DataChannel. 85 | this._rawChannel.close(); 86 | callback(maybeErr); 87 | } 88 | 89 | get label(): string { 90 | return this._rawChannel.getLabel(); 91 | } 92 | 93 | get id(): number { 94 | return this._rawChannel.getId(); 95 | } 96 | 97 | get protocol(): string { 98 | return this._rawChannel.getProtocol(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/node-datachannel.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error no types 2 | import nodeDataChannel = require('../../build/Release/node_datachannel.node'); 3 | export default nodeDataChannel; 4 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Channel { 2 | close(): void; 3 | sendMessage(msg: string): boolean; 4 | sendMessageBinary(buffer: Uint8Array): boolean; 5 | isOpen(): boolean; 6 | bufferedAmount(): number; 7 | maxMessageSize(): number; 8 | setBufferedAmountLowThreshold(newSize: number): void; 9 | onOpen(cb: () => void): void; 10 | onClosed(cb: () => void): void; 11 | onError(cb: (err: string) => void): void; 12 | onBufferedAmountLow(cb: () => void): void; 13 | onMessage(cb: (msg: string | Buffer | ArrayBuffer) => void): void; 14 | } 15 | 16 | export interface WebSocketServerConfiguration { 17 | port?: number; // default 8080 18 | enableTls?: boolean; // default = false; 19 | certificatePemFile?: string; 20 | keyPemFile?: string; 21 | keyPemPass?: string; 22 | bindAddress?: string; 23 | connectionTimeout?: number; // milliseconds 24 | maxMessageSize?: number; 25 | } 26 | 27 | // Enum in d.ts is tricky 28 | export type LogLevel = 'Verbose' | 'Debug' | 'Info' | 'Warning' | 'Error' | 'Fatal'; 29 | 30 | // SCTP Settings 31 | export interface SctpSettings { 32 | recvBufferSize?: number; 33 | sendBufferSize?: number; 34 | maxChunksOnQueue?: number; 35 | initialCongestionWindow?: number; 36 | congestionControlModule?: number; 37 | delayedSackTime?: number; 38 | } 39 | 40 | // Proxy Server 41 | export type ProxyServerType = 'Socks5' | 'Http'; 42 | export interface ProxyServer { 43 | type: ProxyServerType; 44 | ip: string; 45 | port: number; 46 | username?: string; 47 | password?: string; 48 | } 49 | 50 | export type RelayType = 'TurnUdp' | 'TurnTcp' | 'TurnTls'; 51 | 52 | export interface IceServer { 53 | hostname: string; 54 | port: number; 55 | username?: string; 56 | password?: string; 57 | relayType?: RelayType; 58 | } 59 | 60 | export type TransportPolicy = 'all' | 'relay'; 61 | 62 | export interface RtcConfig { 63 | iceServers: (string | IceServer)[]; 64 | proxyServer?: ProxyServer; 65 | bindAddress?: string; 66 | enableIceTcp?: boolean; 67 | enableIceUdpMux?: boolean; 68 | disableAutoNegotiation?: boolean; 69 | forceMediaTransport?: boolean; 70 | portRangeBegin?: number; 71 | portRangeEnd?: number; 72 | maxMessageSize?: number; 73 | mtu?: number; 74 | iceTransportPolicy?: TransportPolicy; 75 | disableFingerprintVerification?: boolean; 76 | disableAutoGathering?: boolean; 77 | certificatePemFile?: string; 78 | keyPemFile?: string; 79 | keyPemPass?: string; 80 | } 81 | 82 | // Lowercase to match the description type string from libdatachannel 83 | export type DescriptionType = 'unspec' | 'offer' | 'answer' | 'pranswer' | 'rollback'; 84 | 85 | export type RTCSdpType = 'answer' | 'offer' | 'pranswer' | 'rollback'; 86 | 87 | export type RTCIceTransportState = 88 | | 'checking' 89 | | 'closed' 90 | | 'completed' 91 | | 'connected' 92 | | 'disconnected' 93 | | 'failed' 94 | | 'new'; 95 | export type RTCPeerConnectionState = 96 | | 'closed' 97 | | 'connected' 98 | | 'connecting' 99 | | 'disconnected' 100 | | 'failed' 101 | | 'new'; 102 | export type RTCIceConnectionState = 103 | | 'checking' 104 | | 'closed' 105 | | 'completed' 106 | | 'connected' 107 | | 'disconnected' 108 | | 'failed' 109 | | 'new'; 110 | export type RTCIceGathererState = 'complete' | 'gathering' | 'new'; 111 | export type RTCIceGatheringState = 'complete' | 'gathering' | 'new'; 112 | export type RTCSignalingState = 113 | | 'closed' 114 | | 'have-local-offer' 115 | | 'have-local-pranswer' 116 | | 'have-remote-offer' 117 | | 'have-remote-pranswer' 118 | | 'stable'; 119 | 120 | export interface LocalDescriptionInit { 121 | iceUfrag?: string; 122 | icePwd?: string; 123 | } 124 | 125 | export interface DataChannelInitConfig { 126 | protocol?: string; 127 | negotiated?: boolean; 128 | id?: number; 129 | unordered?: boolean; // Reliability 130 | maxPacketLifeTime?: number; // Reliability 131 | maxRetransmits?: number; // Reliability 132 | } 133 | 134 | export interface CertificateFingerprint { 135 | /** 136 | * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate/getFingerprints#value 137 | */ 138 | value: string; 139 | /** 140 | * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate/getFingerprints#algorithm 141 | */ 142 | algorithm: 'sha-1' | 'sha-224' | 'sha-256' | 'sha-384' | 'sha-512' | 'md5' | 'md2'; 143 | } 144 | 145 | export interface SelectedCandidateInfo { 146 | address: string; 147 | port: number; 148 | type: string; 149 | transportType: string; 150 | candidate: string; 151 | mid: string; 152 | priority: number; 153 | } 154 | 155 | // Must be same as rtc enum class Direction 156 | export type Direction = 'SendOnly' | 'RecvOnly' | 'SendRecv' | 'Inactive' | 'Unknown'; 157 | 158 | export interface IceUdpMuxRequest { 159 | ufrag: string; 160 | host: string; 161 | port: number; 162 | } 163 | -------------------------------------------------------------------------------- /src/lib/websocket-server.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import nodeDataChannel from './node-datachannel'; 3 | import { WebSocketServerConfiguration } from './types'; 4 | import { WebSocket } from './websocket'; 5 | 6 | export class WebSocketServer extends EventEmitter { 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | #server: any; 9 | #clients: WebSocket[] = []; 10 | 11 | constructor(options: WebSocketServerConfiguration) { 12 | super(); 13 | this.#server = new nodeDataChannel.WebSocketServer(options); 14 | 15 | this.#server.onClient((client) => { 16 | this.emit('client', client); 17 | this.#clients.push(client); 18 | }); 19 | } 20 | 21 | port(): number { 22 | return this.#server?.port() || 0; 23 | } 24 | 25 | stop(): void { 26 | this.#clients.forEach((client) => { 27 | client?.close(); 28 | }); 29 | this.#server?.stop(); 30 | this.#server = null; 31 | this.removeAllListeners(); 32 | } 33 | 34 | onClient(cb: (clientSocket: WebSocket) => void): void { 35 | if (this.#server) this.on('client', cb); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/websocket.ts: -------------------------------------------------------------------------------- 1 | import nodeDataChannel from './node-datachannel'; 2 | import { Channel, WebSocketServerConfiguration } from './types'; 3 | 4 | export interface WebSocket extends Channel { 5 | open(url: string): void; 6 | forceClose(): void; 7 | remoteAddress(): string | undefined; 8 | path(): string | undefined; 9 | 10 | // Channel implementation 11 | close(): void; 12 | sendMessage(msg: string): boolean; 13 | sendMessageBinary(buffer: Uint8Array): boolean; 14 | isOpen(): boolean; 15 | bufferedAmount(): number; 16 | maxMessageSize(): number; 17 | setBufferedAmountLowThreshold(newSize: number): void; 18 | onOpen(cb: () => void): void; 19 | onClosed(cb: () => void): void; 20 | onError(cb: (err: string) => void): void; 21 | onBufferedAmountLow(cb: () => void): void; 22 | onMessage(cb: (msg: string | Buffer) => void): void; 23 | } 24 | export const WebSocket: { 25 | new (config?: WebSocketServerConfiguration): WebSocket; 26 | } = nodeDataChannel.WebSocket; 27 | -------------------------------------------------------------------------------- /src/polyfill/Events.ts: -------------------------------------------------------------------------------- 1 | import RTCDataChannel from './RTCDataChannel'; 2 | import RTCError from './RTCError'; 3 | 4 | export class RTCPeerConnectionIceEvent extends Event implements globalThis.RTCPeerConnectionIceEvent { 5 | #candidate: globalThis.RTCIceCandidate; 6 | 7 | constructor(candidate: globalThis.RTCIceCandidate) { 8 | super('icecandidate'); 9 | 10 | this.#candidate = candidate; 11 | } 12 | 13 | get candidate(): globalThis.RTCIceCandidate { 14 | return this.#candidate; 15 | } 16 | 17 | get url(): string { 18 | return ''; 19 | } 20 | } 21 | 22 | export class RTCDataChannelEvent extends Event implements globalThis.RTCDataChannelEvent { 23 | #channel: RTCDataChannel; 24 | 25 | constructor(type: string = 'datachannel', eventInitDict: globalThis.RTCDataChannelEventInit) { 26 | super(type); 27 | 28 | if (arguments.length === 0) 29 | throw new TypeError( 30 | `Failed to construct 'RTCDataChannelEvent': 2 arguments required, but only ${arguments.length} present.`, 31 | ); 32 | if (typeof eventInitDict !== 'object') 33 | throw new TypeError( 34 | "Failed to construct 'RTCDataChannelEvent': The provided value is not of type 'RTCDataChannelEventInit'.", 35 | ); 36 | if (!eventInitDict.channel) 37 | throw new TypeError( 38 | "Failed to construct 'RTCDataChannelEvent': Failed to read the 'channel' property from 'RTCDataChannelEventInit': Required member is undefined.", 39 | ); 40 | if (eventInitDict.channel.constructor !== RTCDataChannel) 41 | throw new TypeError( 42 | "Failed to construct 'RTCDataChannelEvent': Failed to read the 'channel' property from 'RTCDataChannelEventInit': Failed to convert value to 'RTCDataChannel'.", 43 | ); 44 | 45 | this.#channel = eventInitDict?.channel; 46 | } 47 | 48 | get channel(): RTCDataChannel { 49 | return this.#channel; 50 | } 51 | } 52 | 53 | export class RTCErrorEvent extends Event implements globalThis.RTCErrorEvent { 54 | #error: RTCError; 55 | constructor(type: string, init: globalThis.RTCErrorEventInit) { 56 | if (arguments.length < 2) 57 | throw new TypeError( 58 | `Failed to construct 'RTCErrorEvent': 2 arguments required, but only ${arguments.length} present.`, 59 | ); 60 | if (typeof init !== 'object') 61 | throw new TypeError( 62 | "Failed to construct 'RTCErrorEvent': The provided value is not of type 'RTCErrorEventInit'.", 63 | ); 64 | if (!init.error) 65 | throw new TypeError( 66 | "Failed to construct 'RTCErrorEvent': Failed to read the 'error' property from 'RTCErrorEventInit': Required member is undefined.", 67 | ); 68 | if (init.error.constructor !== RTCError) 69 | throw new TypeError( 70 | "Failed to construct 'RTCErrorEvent': Failed to read the 'error' property from 'RTCErrorEventInit': Failed to convert value to 'RTCError'.", 71 | ); 72 | super(type || 'error'); 73 | this.#error = init.error; 74 | } 75 | 76 | get error(): RTCError { 77 | return this.#error; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/polyfill/Exception.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export class InvalidStateError extends DOMException { 4 | constructor(msg: string) { 5 | super(msg, 'InvalidStateError'); 6 | } 7 | } 8 | 9 | export class TypeError extends DOMException { 10 | constructor(msg: string) { 11 | super(msg, 'TypeError'); 12 | } 13 | } 14 | 15 | export class OperationError extends DOMException { 16 | constructor(msg: string) { 17 | super(msg, 'OperationError'); 18 | } 19 | } 20 | 21 | export class NotFoundError extends DOMException { 22 | constructor(msg: string) { 23 | super(msg, 'NotFoundError'); 24 | } 25 | } 26 | 27 | export class InvalidAccessError extends DOMException { 28 | constructor(msg: string) { 29 | super(msg, 'InvalidAccessError'); 30 | } 31 | } 32 | 33 | export class SyntaxError extends DOMException { 34 | constructor(msg: string) { 35 | super(msg, 'SyntaxError'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/polyfill/README.md: -------------------------------------------------------------------------------- 1 | # WebRTC Polyfills 2 | 3 | WebRTC polyfills to be used for libraries like `simple-peer`. 4 | 5 | # web-platform-tests 6 | 7 | Please check actual situation [here](/test/wpt-tests/) 8 | 9 | ## Example Usage 10 | 11 | > For a native usage example please check test folder. 12 | 13 | `simple-peer` usage example 14 | 15 | ```js 16 | import Peer from 'simple-peer'; 17 | import nodeDatachannelPolyfill from 'node-datachannel/polyfill'; 18 | 19 | var peer1 = new Peer({ initiator: true, wrtc: nodeDatachannelPolyfill }); 20 | var peer2 = new Peer({ wrtc: nodeDatachannelPolyfill }); 21 | 22 | peer1.on('signal', (data) => { 23 | // when peer1 has signaling data, give it to peer2 somehow 24 | peer2.signal(data); 25 | }); 26 | 27 | peer2.on('signal', (data) => { 28 | // when peer2 has signaling data, give it to peer1 somehow 29 | peer1.signal(data); 30 | }); 31 | 32 | peer1.on('connect', () => { 33 | // wait for 'connect' event before using the data channel 34 | peer1.send('hey peer2, how is it going?'); 35 | }); 36 | 37 | peer2.on('data', (data) => { 38 | // got a data channel message 39 | console.log('got a message from peer1: ' + data); 40 | }); 41 | ``` 42 | -------------------------------------------------------------------------------- /src/polyfill/RTCCertificate.ts: -------------------------------------------------------------------------------- 1 | export default class RTCCertificate implements globalThis.RTCCertificate { 2 | #expires: number = 0; 3 | #fingerprints: globalThis.RTCDtlsFingerprint[] = []; 4 | 5 | constructor() { 6 | this.#expires = null; 7 | this.#fingerprints = []; 8 | } 9 | 10 | get expires(): number { 11 | return this.#expires; 12 | } 13 | 14 | getFingerprints(): globalThis.RTCDtlsFingerprint[] { 15 | return this.#fingerprints; 16 | } 17 | 18 | getAlgorithm(): string { 19 | return ''; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/polyfill/RTCDataChannel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as exceptions from './Exception'; 3 | import { DataChannel } from '../lib/index'; 4 | import { RTCErrorEvent } from './Events'; 5 | 6 | export default class RTCDataChannel extends EventTarget implements globalThis.RTCDataChannel { 7 | #dataChannel: DataChannel; 8 | #readyState: globalThis.RTCDataChannelState; 9 | #bufferedAmountLowThreshold: number; 10 | #binaryType: BinaryType; 11 | #maxPacketLifeTime: number | null; 12 | #maxRetransmits: number | null; 13 | #negotiated: boolean; 14 | #ordered: boolean; 15 | #id: number; 16 | #label: string; 17 | #protocol: string; 18 | 19 | #closeRequested = false; 20 | 21 | // events 22 | onbufferedamountlow: globalThis.RTCDataChannel['onbufferedamountlow'] = null; 23 | onclose: globalThis.RTCDataChannel['onclose'] = null; 24 | onclosing: globalThis.RTCDataChannel['onclosing'] = null; 25 | onerror: globalThis.RTCDataChannel['onerror'] = null; 26 | onmessage: globalThis.RTCDataChannel['onmessage'] = null; 27 | onopen: globalThis.RTCDataChannel['onopen'] = null; 28 | 29 | constructor(dataChannel: DataChannel, opts: globalThis.RTCDataChannelInit = {}) { 30 | super(); 31 | 32 | this.#dataChannel = dataChannel; 33 | this.#binaryType = 'blob'; 34 | this.#readyState = this.#dataChannel.isOpen() ? 'open' : 'connecting'; 35 | this.#bufferedAmountLowThreshold = 0; 36 | this.#maxPacketLifeTime = opts.maxPacketLifeTime ?? null; 37 | this.#maxRetransmits = opts.maxRetransmits ?? null; 38 | this.#negotiated = opts.negotiated ?? false; 39 | this.#ordered = opts.ordered ?? true; 40 | this.#id = this.#dataChannel.getId(); 41 | this.#label = this.#dataChannel.getLabel(); 42 | this.#protocol = this.#dataChannel.getProtocol(); 43 | 44 | // forward dataChannel events 45 | this.#dataChannel.onOpen(() => { 46 | this.#readyState = 'open'; 47 | this.dispatchEvent(new Event('open', {})); 48 | }); 49 | 50 | this.#dataChannel.onClosed(() => { 51 | // Simulate closing event 52 | if (!this.#closeRequested) { 53 | this.#readyState = 'closing'; 54 | this.dispatchEvent(new Event('closing')); 55 | } 56 | 57 | setImmediate(() => { 58 | this.#readyState = 'closed'; 59 | this.dispatchEvent(new Event('close')); 60 | }); 61 | }); 62 | 63 | this.#dataChannel.onError((msg) => { 64 | this.dispatchEvent( 65 | new RTCErrorEvent('error', { 66 | error: new RTCError( 67 | { 68 | errorDetail: 'data-channel-failure', 69 | }, 70 | msg, 71 | ), 72 | }), 73 | ); 74 | }); 75 | 76 | this.#dataChannel.onBufferedAmountLow(() => { 77 | this.dispatchEvent(new Event('bufferedamountlow')); 78 | }); 79 | 80 | this.#dataChannel.onMessage((data) => { 81 | if (ArrayBuffer.isView(data)) { 82 | if (this.binaryType == 'arraybuffer') data = data.buffer; 83 | else data = Buffer.from(data.buffer); 84 | } 85 | 86 | this.dispatchEvent(new MessageEvent('message', { data })); 87 | }); 88 | 89 | // forward events to properties 90 | this.addEventListener('message', (e) => { 91 | if (this.onmessage) this.onmessage(e as MessageEvent); 92 | }); 93 | this.addEventListener('bufferedamountlow', (e) => { 94 | if (this.onbufferedamountlow) this.onbufferedamountlow(e); 95 | }); 96 | this.addEventListener('error', (e) => { 97 | if (this.onerror) this.onerror(e as RTCErrorEvent); 98 | }); 99 | this.addEventListener('close', (e) => { 100 | if (this.onclose) this.onclose(e); 101 | }); 102 | this.addEventListener('closing', (e) => { 103 | if (this.onclosing) this.onclosing(e); 104 | }); 105 | this.addEventListener('open', (e) => { 106 | if (this.onopen) this.onopen(e); 107 | }); 108 | } 109 | 110 | set binaryType(type) { 111 | if (type !== 'blob' && type !== 'arraybuffer') { 112 | throw new DOMException( 113 | "Failed to set the 'binaryType' property on 'RTCDataChannel': Unknown binary type : " + 114 | type, 115 | 'TypeMismatchError', 116 | ); 117 | } 118 | this.#binaryType = type; 119 | } 120 | 121 | get binaryType(): BinaryType { 122 | return this.#binaryType; 123 | } 124 | 125 | get bufferedAmount(): number { 126 | return this.#dataChannel.bufferedAmount(); 127 | } 128 | 129 | get bufferedAmountLowThreshold(): number { 130 | return this.#bufferedAmountLowThreshold; 131 | } 132 | 133 | set bufferedAmountLowThreshold(value) { 134 | const number = Number(value) || 0; 135 | this.#bufferedAmountLowThreshold = number; 136 | this.#dataChannel.setBufferedAmountLowThreshold(number); 137 | } 138 | 139 | get id(): number | null { 140 | return this.#id; 141 | } 142 | 143 | get label(): string { 144 | return this.#label; 145 | } 146 | 147 | get maxPacketLifeTime(): number | null { 148 | return this.#maxPacketLifeTime; 149 | } 150 | 151 | get maxRetransmits(): number | null { 152 | return this.#maxRetransmits; 153 | } 154 | 155 | get negotiated(): boolean { 156 | return this.#negotiated; 157 | } 158 | 159 | get ordered(): boolean { 160 | return this.#ordered; 161 | } 162 | 163 | get protocol(): string { 164 | return this.#protocol; 165 | } 166 | 167 | get readyState(): globalThis.RTCDataChannelState { 168 | return this.#readyState; 169 | } 170 | 171 | send(data): void { 172 | if (this.#readyState !== 'open') { 173 | throw new exceptions.InvalidStateError( 174 | "Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel.readyState is not 'open'", 175 | ); 176 | } 177 | 178 | // Needs network error, type error implemented 179 | if (typeof data === 'string') { 180 | this.#dataChannel.sendMessage(data); 181 | } else if (data instanceof Blob) { 182 | data.arrayBuffer().then((ab) => { 183 | if (process?.versions?.bun) { 184 | this.#dataChannel.sendMessageBinary(Buffer.from(ab)); 185 | } else { 186 | this.#dataChannel.sendMessageBinary(new Uint8Array(ab)); 187 | } 188 | }); 189 | } else if (data instanceof Uint8Array) { 190 | this.#dataChannel.sendMessageBinary(data); 191 | } else { 192 | if (process?.versions?.bun) { 193 | this.#dataChannel.sendMessageBinary(Buffer.from(data)); 194 | } else { 195 | this.#dataChannel.sendMessageBinary(new Uint8Array(data)); 196 | } 197 | } 198 | } 199 | 200 | close(): void { 201 | this.#closeRequested = true; 202 | setImmediate(() => { 203 | this.#dataChannel.close(); 204 | }); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/polyfill/RTCDtlsTransport.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import RTCIceTransport from './RTCIceTransport'; 3 | import RTCPeerConnection from './RTCPeerConnection'; 4 | 5 | export default class RTCDtlsTransport extends EventTarget implements globalThis.RTCDtlsTransport { 6 | #pc: RTCPeerConnection = null; 7 | #iceTransport = null; 8 | 9 | onstatechange: globalThis.RTCDtlsTransport['onstatechange'] = null; 10 | onerror: globalThis.RTCDtlsTransport['onstatechange'] = null; 11 | 12 | constructor(init: { pc: RTCPeerConnection }) { 13 | super(); 14 | this.#pc = init.pc; 15 | 16 | this.#iceTransport = new RTCIceTransport({ 17 | pc: init.pc 18 | }); 19 | 20 | // forward peerConnection events 21 | this.#pc.addEventListener('connectionstatechange', () => { 22 | const e = new Event('statechange'); 23 | this.dispatchEvent(e); 24 | this.onstatechange?.(e); 25 | }); 26 | } 27 | 28 | get iceTransport(): globalThis.RTCIceTransport { 29 | return this.#iceTransport; 30 | } 31 | 32 | get state(): globalThis.RTCDtlsTransportState { 33 | // reduce state from new, connecting, connected, disconnected, failed, closed, unknown 34 | // to RTCDtlsTRansport states new, connecting, connected, closed, failed 35 | let state = this.#pc ? this.#pc.connectionState : 'new'; 36 | if (state === 'disconnected') { 37 | state = 'closed'; 38 | } 39 | return state; 40 | } 41 | 42 | getRemoteCertificates(): ArrayBuffer[] { 43 | // TODO: implement 44 | return [new ArrayBuffer(0)]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/polyfill/RTCError.ts: -------------------------------------------------------------------------------- 1 | export default class RTCError extends DOMException implements globalThis.RTCError { 2 | #errorDetail: globalThis.RTCErrorDetailType; 3 | #receivedAlert: number | null; 4 | #sctpCauseCode: number | null; 5 | #sdpLineNumber: number | null; 6 | #sentAlert: number | null; 7 | #httpRequestStatusCode: number | null; 8 | 9 | constructor(init: globalThis.RTCErrorInit, message?: string) { 10 | super(message, 'OperationError'); 11 | 12 | if (!init || !init.errorDetail) 13 | throw new TypeError('Cannot construct RTCError, errorDetail is required'); 14 | if ( 15 | [ 16 | 'data-channel-failure', 17 | 'dtls-failure', 18 | 'fingerprint-failure', 19 | 'hardware-encoder-error', 20 | 'hardware-encoder-not-available', 21 | 'sctp-failure', 22 | 'sdp-syntax-error', 23 | ].indexOf(init.errorDetail) === -1 24 | ) 25 | throw new TypeError('Cannot construct RTCError, errorDetail is invalid'); 26 | 27 | this.#errorDetail = init.errorDetail; 28 | this.#receivedAlert = init.receivedAlert ?? null; 29 | this.#sctpCauseCode = init.sctpCauseCode ?? null; 30 | this.#sdpLineNumber = init.sdpLineNumber ?? null; 31 | this.#sentAlert = init.sentAlert ?? null; 32 | this.#httpRequestStatusCode = init.httpRequestStatusCode ?? null; 33 | } 34 | 35 | get errorDetail(): globalThis.RTCErrorDetailType { 36 | return this.#errorDetail; 37 | } 38 | 39 | set errorDetail(_value) { 40 | throw new TypeError('Cannot set errorDetail, it is read-only'); 41 | } 42 | 43 | get receivedAlert(): number | null { 44 | return this.#receivedAlert; 45 | } 46 | 47 | set receivedAlert(_value) { 48 | throw new TypeError('Cannot set receivedAlert, it is read-only'); 49 | } 50 | 51 | get sctpCauseCode(): number | null { 52 | return this.#sctpCauseCode; 53 | } 54 | 55 | set sctpCauseCode(_value) { 56 | throw new TypeError('Cannot set sctpCauseCode, it is read-only'); 57 | } 58 | 59 | get httpRequestStatusCode(): number | null { 60 | return this.#httpRequestStatusCode; 61 | } 62 | 63 | get sdpLineNumber(): number | null { 64 | return this.#sdpLineNumber; 65 | } 66 | 67 | set sdpLineNumber(_value) { 68 | throw new TypeError('Cannot set sdpLineNumber, it is read-only'); 69 | } 70 | 71 | get sentAlert(): number | null { 72 | return this.#sentAlert; 73 | } 74 | 75 | set sentAlert(_value) { 76 | throw new TypeError('Cannot set sentAlert, it is read-only'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/polyfill/RTCIceCandidate.ts: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/docs/Web/API/RTCIceCandidate 2 | // 3 | // Example: candidate:123456 1 UDP 123456 192.168.1.1 12345 typ host raddr=10.0.0.1 rport=54321 generation 0 4 | 5 | export default class RTCIceCandidate implements globalThis.RTCIceCandidate { 6 | #address: string | null; 7 | #candidate: string; 8 | #component: globalThis.RTCIceComponent | null; 9 | #foundation: string | null; 10 | #port: number | null; 11 | #priority: number | null; 12 | #protocol: globalThis.RTCIceProtocol | null; 13 | #relatedAddress: string | null; 14 | #relatedPort: number | null; 15 | #sdpMLineIndex: number | null; 16 | #sdpMid: string | null; 17 | #tcpType: globalThis.RTCIceTcpCandidateType | null; 18 | #type: globalThis.RTCIceCandidateType | null; 19 | #usernameFragment: string | null; 20 | 21 | constructor({ 22 | candidate, 23 | sdpMLineIndex, 24 | sdpMid, 25 | usernameFragment, 26 | }: globalThis.RTCIceCandidateInit) { 27 | if (sdpMLineIndex == null && sdpMid == null) 28 | throw new TypeError('At least one of sdpMLineIndex or sdpMid must be specified'); 29 | 30 | this.#candidate = candidate === null ? 'null' : (candidate ?? ''); 31 | this.#sdpMLineIndex = sdpMLineIndex ?? null; 32 | this.#sdpMid = sdpMid ?? null; 33 | this.#usernameFragment = usernameFragment ?? null; 34 | 35 | if (candidate) { 36 | const fields = candidate.split(' '); 37 | this.#foundation = fields[0]!.replace('candidate:', ''); // remove text candidate: 38 | this.#component = fields[1] == '1' ? 'rtp' : 'rtcp'; 39 | this.#protocol = fields[2] as globalThis.RTCIceProtocol; 40 | this.#priority = parseInt(fields[3], 10); 41 | this.#address = fields[4]; 42 | this.#port = parseInt(fields[5], 10); 43 | this.#type = fields[7] as globalThis.RTCIceCandidateType; 44 | this.#tcpType = null; 45 | this.#relatedAddress = null; 46 | this.#relatedPort = null; 47 | 48 | // Parse the candidate string to extract relatedPort and relatedAddress 49 | for (let i = 8; i < fields.length; i++) { 50 | const field = fields[i]; 51 | if (field === 'raddr') { 52 | this.#relatedAddress = fields[i + 1]; 53 | } else if (field === 'rport') { 54 | this.#relatedPort = parseInt(fields[i + 1], 10); 55 | } 56 | 57 | if (this.#protocol === 'tcp' && field === 'tcptype') { 58 | this.#tcpType = fields[i + 1] as globalThis.RTCIceTcpCandidateType; 59 | } 60 | } 61 | } 62 | } 63 | 64 | get address(): string | null { 65 | return this.#address ?? null; 66 | } 67 | 68 | get candidate(): string { 69 | return this.#candidate; 70 | } 71 | 72 | get component(): globalThis.RTCIceComponent | null { 73 | return this.#component; 74 | } 75 | 76 | get foundation(): string | null { 77 | return this.#foundation ?? null; 78 | } 79 | 80 | get port(): number | null { 81 | return this.#port ?? null; 82 | } 83 | 84 | get priority(): number | null { 85 | return this.#priority ?? null; 86 | } 87 | 88 | get protocol(): globalThis.RTCIceProtocol | null { 89 | return this.#protocol ?? null; 90 | } 91 | 92 | get relatedAddress(): string | null { 93 | return this.#relatedAddress; 94 | } 95 | 96 | get relatedPort(): number | null { 97 | return this.#relatedPort ?? null; 98 | } 99 | 100 | get sdpMLineIndex(): number | null { 101 | return this.#sdpMLineIndex; 102 | } 103 | 104 | get sdpMid(): string | null { 105 | return this.#sdpMid; 106 | } 107 | 108 | get tcpType(): globalThis.RTCIceTcpCandidateType | null { 109 | return this.#tcpType; 110 | } 111 | 112 | get type(): globalThis.RTCIceCandidateType | null { 113 | return this.#type ?? null; 114 | } 115 | 116 | get usernameFragment(): string | null { 117 | return this.#usernameFragment; 118 | } 119 | 120 | toJSON(): globalThis.RTCIceCandidateInit { 121 | return { 122 | candidate: this.#candidate, 123 | sdpMLineIndex: this.#sdpMLineIndex, 124 | sdpMid: this.#sdpMid, 125 | usernameFragment: this.#usernameFragment, 126 | }; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/polyfill/RTCIceTransport.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import RTCIceCandidate from './RTCIceCandidate'; 3 | import RTCPeerConnection from './RTCPeerConnection'; 4 | 5 | export default class RTCIceTransport extends EventTarget implements globalThis.RTCIceTransport { 6 | #pc: RTCPeerConnection = null; 7 | 8 | ongatheringstatechange: globalThis.RTCIceTransport['ongatheringstatechange'] = null; 9 | onselectedcandidatepairchange: globalThis.RTCIceTransport['onselectedcandidatepairchange'] = null; 10 | onstatechange: globalThis.RTCIceTransport['onstatechange'] = null; 11 | 12 | constructor(init: { pc: RTCPeerConnection }) { 13 | super(); 14 | this.#pc = init.pc; 15 | 16 | this.#pc.addEventListener('icegatheringstatechange', () => { 17 | const e = new Event('gatheringstatechange'); 18 | this.dispatchEvent(e); 19 | this.ongatheringstatechange?.(e); 20 | }); 21 | this.#pc.addEventListener('iceconnectionstatechange', () => { 22 | const e = new Event('statechange'); 23 | this.dispatchEvent(e); 24 | this.onstatechange?.(e); 25 | }); 26 | } 27 | 28 | get component(): globalThis.RTCIceComponent { 29 | const cp = this.getSelectedCandidatePair(); 30 | if (!cp?.local) return null; 31 | return cp.local.component; 32 | } 33 | 34 | get gatheringState(): globalThis.RTCIceGatheringState { 35 | return this.#pc ? this.#pc.iceGatheringState : 'new'; 36 | } 37 | 38 | get role(): globalThis.RTCIceRole { 39 | return this.#pc.localDescription!.type == 'offer' ? 'controlling' : 'controlled'; 40 | } 41 | 42 | get state(): globalThis.RTCIceTransportState { 43 | return this.#pc ? this.#pc.iceConnectionState : 'new'; 44 | } 45 | 46 | getLocalCandidates(): globalThis.RTCIceCandidate[] { 47 | return this.#pc?.ext_localCandidates ?? []; 48 | } 49 | 50 | getLocalParameters(): RTCIceParameters | null { 51 | return new RTCIceParameters( 52 | new RTCIceCandidate({ 53 | candidate: this.#pc.selectedCandidatePair()!.local.candidate, 54 | sdpMLineIndex: 0, 55 | }), 56 | ); 57 | } 58 | 59 | getRemoteCandidates(): globalThis.RTCIceCandidate[] { 60 | return this.#pc?.ext_remoteCandidates ?? []; 61 | } 62 | 63 | getRemoteParameters(): any { 64 | /** */ 65 | } 66 | 67 | getSelectedCandidatePair(): globalThis.RTCIceCandidatePair | null { 68 | const cp = this.#pc?.selectedCandidatePair(); 69 | if (!cp) return null; 70 | return { 71 | local: new RTCIceCandidate({ 72 | candidate: cp.local.candidate, 73 | sdpMid: cp.local.mid, 74 | }), 75 | remote: new RTCIceCandidate({ 76 | candidate: cp.remote.candidate, 77 | sdpMid: cp.remote.mid, 78 | }), 79 | }; 80 | } 81 | } 82 | 83 | export class RTCIceParameters implements globalThis.RTCIceParameters { 84 | usernameFragment = ''; 85 | password = ''; 86 | constructor({ usernameFragment, password = '' }) { 87 | this.usernameFragment = usernameFragment; 88 | this.password = password; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/polyfill/RTCSctpTransport.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import RTCDtlsTransport from './RTCDtlsTransport'; 3 | import RTCPeerConnection from './RTCPeerConnection'; 4 | 5 | export default class RTCSctpTransport extends EventTarget implements globalThis.RTCSctpTransport { 6 | #pc: RTCPeerConnection = null; 7 | #transport: globalThis.RTCDtlsTransport = null; 8 | 9 | onstatechange: globalThis.RTCSctpTransport['onstatechange'] = null; 10 | 11 | constructor(initial: { pc: RTCPeerConnection }) { 12 | super(); 13 | this.#pc = initial.pc; 14 | 15 | this.#transport = new RTCDtlsTransport({ 16 | pc: initial.pc 17 | }); 18 | 19 | this.#pc.addEventListener('connectionstatechange', () => { 20 | const e = new Event('statechange'); 21 | this.dispatchEvent(e); 22 | this.onstatechange?.(e); 23 | }); 24 | } 25 | 26 | get maxChannels(): number | null { 27 | if (this.state !== 'connected') return null; 28 | return this.#pc.ext_maxDataChannelId; 29 | } 30 | 31 | get maxMessageSize(): number { 32 | if (this.state !== 'connected') return null; 33 | return this.#pc?.ext_maxMessageSize ?? 65536;; 34 | } 35 | 36 | get state(): globalThis.RTCSctpTransportState { 37 | // reduce state from new, connecting, connected, disconnected, failed, closed, unknown 38 | // to RTCSctpTransport states connecting, connected, closed 39 | let state = this.#pc.connectionState; 40 | if (state === 'new' || state === 'connecting') { 41 | state = 'connecting'; 42 | } else if (state === 'disconnected' || state === 'failed' || state === 'closed') { 43 | state = 'closed'; 44 | } 45 | return state; 46 | } 47 | 48 | get transport(): globalThis.RTCDtlsTransport { 49 | return this.#transport; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/polyfill/RTCSessionDescription.ts: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/docs/Web/API/RTCSessionDescription 2 | // 3 | // Example usage 4 | // const init = { 5 | // type: 'offer', 6 | // sdp: 'v=0\r\no=- 1234567890 1234567890 IN IP4 192.168.1.1\r\ns=-\r\nt=0 0\r\na=ice-ufrag:abcd\r\na=ice-pwd:efgh\r\n' 7 | // }; 8 | 9 | export default class RTCSessionDescription implements globalThis.RTCSessionDescriptionInit { 10 | #type: globalThis.RTCSdpType; 11 | #sdp: string; 12 | 13 | constructor(init: globalThis.RTCSessionDescriptionInit) { 14 | this.#type = init?.type; 15 | this.#sdp = init?.sdp ?? ''; 16 | } 17 | 18 | get type(): globalThis.RTCSdpType { 19 | return this.#type; 20 | } 21 | 22 | set type(type) { 23 | if (type !== 'offer' && type !== 'answer' && type !== 'pranswer' && type !== 'rollback') { 24 | throw new TypeError( 25 | `Failed to set the 'type' property on 'RTCSessionDescription': The provided value '${type}' is not a valid enum value of type RTCSdpType.`, 26 | ); 27 | } 28 | this.#type = type; 29 | } 30 | 31 | get sdp(): string { 32 | return this.#sdp; 33 | } 34 | 35 | toJSON(): globalThis.RTCSessionDescriptionInit { 36 | return { 37 | sdp: this.#sdp, 38 | type: this.#type, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/polyfill/index.ts: -------------------------------------------------------------------------------- 1 | import RTCCertificate from './RTCCertificate'; 2 | import RTCDataChannel from './RTCDataChannel'; 3 | import RTCDtlsTransport from './RTCDtlsTransport'; 4 | import RTCIceCandidate from './RTCIceCandidate'; 5 | import RTCIceTransport from './RTCIceTransport'; 6 | import RTCPeerConnection from './RTCPeerConnection'; 7 | import RTCSctpTransport from './RTCSctpTransport'; 8 | import RTCSessionDescription from './RTCSessionDescription'; 9 | import { RTCDataChannelEvent, RTCPeerConnectionIceEvent } from './Events'; 10 | import RTCError from './RTCError'; 11 | 12 | export { 13 | RTCCertificate, 14 | RTCDataChannel, 15 | RTCDtlsTransport, 16 | RTCIceCandidate, 17 | RTCIceTransport, 18 | RTCPeerConnection, 19 | RTCSctpTransport, 20 | RTCSessionDescription, 21 | RTCDataChannelEvent, 22 | RTCPeerConnectionIceEvent, 23 | RTCError, 24 | }; 25 | 26 | export default { 27 | RTCCertificate, 28 | RTCDataChannel, 29 | RTCDtlsTransport, 30 | RTCIceCandidate, 31 | RTCIceTransport, 32 | RTCPeerConnection, 33 | RTCSctpTransport, 34 | RTCSessionDescription, 35 | RTCDataChannelEvent, 36 | RTCPeerConnectionIceEvent, 37 | RTCError, 38 | }; 39 | -------------------------------------------------------------------------------- /test/connectivity.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, DataChannel, initLogger, PeerConnection } from '../src/lib/index'; 2 | 3 | initLogger('Debug'); 4 | 5 | let dc1: DataChannel = null; 6 | let dc2: DataChannel = null; 7 | 8 | // "iceServers" option is an array of stun/turn server urls 9 | // Examples; 10 | // STUN Server Example : stun:stun.l.google.com:19302 11 | // TURN Server Example : turn:USERNAME:PASSWORD@TURN_IP_OR_ADDRESS:PORT 12 | // TURN Server Example (TCP) : turn:USERNAME:PASSWORD@TURN_IP_OR_ADDRESS:PORT?transport=tcp 13 | // TURN Server Example (TLS) : turns:USERNAME:PASSWORD@TURN_IP_OR_ADDRESS:PORT 14 | const peer1: PeerConnection = new PeerConnection('Peer1', { 15 | iceServers: ['stun:stun.l.google.com:19302'], 16 | }); 17 | 18 | // Set Callbacks 19 | peer1.onStateChange((state) => { 20 | console.log('Peer1 State:', state); 21 | }); 22 | peer1.onIceStateChange((state) => { 23 | console.log('Peer1 IceState:', state); 24 | }); 25 | peer1.onGatheringStateChange((state) => { 26 | console.log('Peer1 GatheringState:', state); 27 | }); 28 | peer1.onLocalDescription((sdp, type) => { 29 | console.log('Peer1 SDP:', sdp, ' Type:', type); 30 | peer2.setRemoteDescription(sdp, type); 31 | }); 32 | peer1.onLocalCandidate((candidate, mid) => { 33 | console.log('Peer1 Candidate:', candidate); 34 | peer2.addRemoteCandidate(candidate, mid); 35 | }); 36 | 37 | const peer2 = new PeerConnection('Peer2', { 38 | iceServers: ['stun:stun.l.google.com:19302'], 39 | }); 40 | 41 | // Set Callbacks 42 | peer2.onStateChange((state) => { 43 | console.log('Peer2 State:', state); 44 | }); 45 | peer2.onIceStateChange((state) => { 46 | console.log('Peer2 IceState:', state); 47 | }); 48 | peer2.onGatheringStateChange((state) => { 49 | console.log('Peer2 GatheringState:', state); 50 | }); 51 | peer2.onLocalDescription((sdp, type) => { 52 | console.log('Peer2 SDP:', sdp, ' Type:', type); 53 | peer1.setRemoteDescription(sdp, type); 54 | }); 55 | peer2.onLocalCandidate((candidate, mid) => { 56 | console.log('Peer2 Candidate:', candidate); 57 | peer1.addRemoteCandidate(candidate, mid); 58 | }); 59 | peer2.onDataChannel((dc) => { 60 | console.log('Peer2 Got DataChannel: ', dc.getLabel()); 61 | dc2 = dc; 62 | dc2.onMessage((msg) => { 63 | console.log('Peer2 Received Msg:', msg); 64 | }); 65 | dc2.sendMessage('Hello From Peer2'); 66 | dc2.onClosed(() => { 67 | console.log('dc2 closed'); 68 | }); 69 | }); 70 | 71 | // Create DataChannel 72 | dc1 = peer1.createDataChannel('test'); 73 | dc1.onOpen(() => { 74 | dc1.sendMessage('Hello from Peer1'); 75 | // Binary message: Use sendMessageBinary(Buffer) 76 | }); 77 | dc1.onMessage((msg) => { 78 | console.log('Peer1 Received Msg:', msg); 79 | }); 80 | dc1.onClosed(() => { 81 | console.log('dc1 closed'); 82 | }); 83 | 84 | setTimeout(() => { 85 | if (dc1) dc1.close(); 86 | if (dc2) dc2.close(); 87 | peer1.close(); 88 | peer2.close(); 89 | cleanup(); 90 | }, 5 * 1000); 91 | -------------------------------------------------------------------------------- /test/fixtures/event-promise.ts: -------------------------------------------------------------------------------- 1 | export interface EventPromiseOptions { 2 | errorEvent?: string; 3 | } 4 | 5 | export async function eventPromise( 6 | emitter: EventTarget, 7 | event: string, 8 | opts?: EventPromiseOptions, 9 | ): Promise { 10 | return new Promise((resolve, reject) => { 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | emitter.addEventListener(event, (evt: any) => { 13 | resolve(evt); 14 | }); 15 | emitter.addEventListener(opts?.errorEvent ?? 'error', (err) => { 16 | reject(err); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/jest-tests/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as nodeDataChannel from '../../src/lib/index'; 2 | 3 | describe('Module Definition', () => { 4 | test('Module Defined', () => { 5 | expect(nodeDataChannel).toBeDefined(); 6 | expect(nodeDataChannel.initLogger).toBeDefined(); 7 | expect(nodeDataChannel.PeerConnection).toBeDefined(); 8 | expect(typeof nodeDataChannel.PeerConnection).toBe('function'); 9 | expect(nodeDataChannel.DataChannel).toBeDefined(); 10 | expect(typeof nodeDataChannel.DataChannel).toBe('function'); 11 | }); 12 | }); 13 | 14 | describe('PeerConnection Classes', () => { 15 | test('Create PeerConnection', () => { 16 | const peer = new nodeDataChannel.PeerConnection('Peer', { 17 | iceServers: ['stun:stun.l.google.com:19302'], 18 | }); 19 | expect(peer).toBeDefined(); 20 | expect(peer.onStateChange).toBeDefined(); 21 | expect(peer.createDataChannel).toBeDefined(); 22 | 23 | peer.close(); 24 | }); 25 | 26 | test('Create Data Channel', () => { 27 | const peer = new nodeDataChannel.PeerConnection('Peer', { 28 | iceServers: ['stun:stun.l.google.com:19302'], 29 | }); 30 | const dc = peer.createDataChannel('test', { protocol: 'test-protocol' }); 31 | expect(dc).toBeDefined(); 32 | expect(dc.getId()).toBeDefined(); 33 | expect(dc.getProtocol()).toBe('test-protocol'); 34 | expect(dc.getLabel()).toBe('test'); 35 | expect(dc.onOpen).toBeDefined(); 36 | expect(dc.onMessage).toBeDefined(); 37 | 38 | dc.close(); 39 | peer.close(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/jest-tests/multiple-run.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import * as nodeDataChannel from '../../src/lib/index'; 3 | 4 | describe('P2P', () => { 5 | // Default is 5000 ms but we need more 6 | jest.setTimeout(12000); 7 | 8 | test.each(Array(100).fill(0))('P2P Test-%p', () => { 9 | return new Promise((done) => { 10 | const peer1 = new nodeDataChannel.PeerConnection('Peer1', { 11 | iceServers: ['stun:stun.l.google.com:19302'], 12 | }); 13 | const peer2 = new nodeDataChannel.PeerConnection('Peer2', { 14 | iceServers: ['stun:stun.l.google.com:19302'], 15 | }); 16 | let dc1 = null; 17 | let dc2 = null; 18 | 19 | // Mocks 20 | const p1DCMessageMock = jest.fn(); 21 | const p2DCMessageMock = jest.fn(); 22 | 23 | // Set Callbacks 24 | peer1.onStateChange(() => { 25 | /** */ 26 | }); 27 | peer1.onGatheringStateChange(() => { 28 | /** */ 29 | }); 30 | peer1.onLocalDescription((sdp, type) => { 31 | peer2.setRemoteDescription(sdp, type); 32 | }); 33 | peer1.onLocalCandidate((candidate, mid) => { 34 | peer2.addRemoteCandidate(candidate, mid); 35 | }); 36 | 37 | // Set Callbacks 38 | peer2.onStateChange(() => { 39 | /** */ 40 | }); 41 | peer2.onGatheringStateChange(() => { 42 | /** */ 43 | }); 44 | peer2.onLocalDescription((sdp, type) => { 45 | peer1.setRemoteDescription(sdp, type); 46 | }); 47 | peer2.onLocalCandidate((candidate, mid) => { 48 | peer1.addRemoteCandidate(candidate, mid); 49 | }); 50 | peer2.onDataChannel((dc) => { 51 | dc2 = dc; 52 | dc2.onMessage((msg) => { 53 | p2DCMessageMock(msg); 54 | dc2.sendMessage('Hello From Peer2'); 55 | }); 56 | }); 57 | 58 | dc1 = peer1.createDataChannel('test-p2p'); 59 | dc1.onOpen(() => { 60 | dc1.sendMessage('Hello From Peer1'); 61 | }); 62 | dc1.onMessage((msg) => { 63 | p1DCMessageMock(msg); 64 | peer1.close(); 65 | peer2.close(); 66 | 67 | // DataChannel 68 | expect(p1DCMessageMock.mock.calls.length).toBe(1); 69 | expect(p1DCMessageMock.mock.calls[0][0]).toEqual('Hello From Peer2'); 70 | expect(p2DCMessageMock.mock.calls.length).toBe(1); 71 | expect(p2DCMessageMock.mock.calls[0][0]).toEqual('Hello From Peer1'); 72 | 73 | done(); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/jest-tests/p2p.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import * as nodeDataChannel from '../../src/lib/index'; 3 | 4 | describe('P2P', () => { 5 | // Default is 5000 ms but we need more 6 | jest.setTimeout(30000); 7 | 8 | test('P2P Test', () => { 9 | return new Promise((done) => { 10 | const peer1 = new nodeDataChannel.PeerConnection('Peer1', { 11 | iceServers: ['stun:stun.l.google.com:19302'], 12 | }); 13 | const peer2 = new nodeDataChannel.PeerConnection('Peer2', { 14 | iceServers: ['stun:stun.l.google.com:19302'], 15 | }); 16 | let dc1 = null; 17 | let dc2 = null; 18 | 19 | // Mocks 20 | const p1StateMock = jest.fn(); 21 | const p1GatheringStateMock = jest.fn(); 22 | const p2StateMock = jest.fn(); 23 | const p2GatheringStateMock = jest.fn(); 24 | const p1SDPMock = jest.fn(); 25 | const p1CandidateMock = jest.fn(); 26 | const p2SDPMock = jest.fn(); 27 | const p2CandidateMock = jest.fn(); 28 | 29 | const p1DCMock = jest.fn(); 30 | const p1DCMessageMock = jest.fn(); 31 | const p2DCMock = jest.fn(); 32 | const p2DCMessageMock = jest.fn(); 33 | 34 | // Set Callbacks 35 | peer1.onStateChange(p1StateMock); 36 | peer1.onGatheringStateChange(p1GatheringStateMock); 37 | peer1.onLocalDescription((sdp, type) => { 38 | p1SDPMock(); 39 | peer2.setRemoteDescription(sdp, type); 40 | }); 41 | peer1.onLocalCandidate((candidate, mid) => { 42 | p1CandidateMock(); 43 | peer2.addRemoteCandidate(candidate, mid); 44 | }); 45 | 46 | // Set Callbacks 47 | peer2.onStateChange(p2StateMock); 48 | peer2.onGatheringStateChange(p2GatheringStateMock); 49 | peer2.onLocalDescription((sdp, type) => { 50 | p2SDPMock(); 51 | peer1.setRemoteDescription(sdp, type); 52 | }); 53 | peer2.onLocalCandidate((candidate, mid) => { 54 | p2CandidateMock(); 55 | peer1.addRemoteCandidate(candidate, mid); 56 | }); 57 | peer2.onDataChannel((dc) => { 58 | p2DCMock(); 59 | dc2 = dc; 60 | dc2.onMessage((msg) => { 61 | p2DCMessageMock(msg); 62 | dc2.sendMessage('Hello From Peer2'); 63 | }); 64 | }); 65 | 66 | dc1 = peer1.createDataChannel('test-p2p'); 67 | dc1.onOpen(() => { 68 | p1DCMock(); 69 | dc1.sendMessage('Hello From Peer1'); 70 | }); 71 | dc1.onMessage((msg) => { 72 | p1DCMessageMock(msg); 73 | peer1.close(); 74 | peer2.close(); 75 | 76 | // State Callbacks 77 | expect(p1StateMock.mock.calls.length).toBeGreaterThanOrEqual(1); 78 | expect(p1GatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); 79 | expect(p2StateMock.mock.calls.length).toBeGreaterThanOrEqual(1); 80 | expect(p2GatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); 81 | 82 | // SDP 83 | expect(p1SDPMock.mock.calls.length).toBe(1); 84 | expect(p2SDPMock.mock.calls.length).toBe(1); 85 | 86 | // Candidates 87 | expect(p1CandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); 88 | expect(p2CandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); 89 | 90 | // DataChannel 91 | expect(p1DCMock.mock.calls.length).toBe(1); 92 | expect(p1DCMessageMock.mock.calls.length).toBe(1); 93 | expect(p1DCMessageMock.mock.calls[0][0]).toEqual('Hello From Peer2'); 94 | expect(p2DCMock.mock.calls.length).toBe(1); 95 | 96 | expect(p2DCMessageMock.mock.calls.length).toBe(1); 97 | expect(p2DCMessageMock.mock.calls[0][0]).toEqual('Hello From Peer1'); 98 | 99 | done(); 100 | }); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/jest-tests/streams.test.ts: -------------------------------------------------------------------------------- 1 | import * as nodeDataChannel from '../../src/lib/index'; 2 | 3 | function waitForGathering(peer: nodeDataChannel.PeerConnection): Promise { 4 | return new Promise((resolve) => { 5 | peer.onGatheringStateChange((state) => { 6 | if (state === 'complete') resolve(); 7 | }); 8 | // Handle race conditions where gathering has already completed 9 | if (peer.gatheringState() === 'complete') resolve(); 10 | 11 | resolve(); 12 | }); 13 | } 14 | 15 | describe('DataChannel streams', () => { 16 | test('can build an echo pipeline', async () => { 17 | const clientPeer = new nodeDataChannel.PeerConnection('Client', { 18 | iceServers: [], 19 | }); 20 | const echoPeer = new nodeDataChannel.PeerConnection('Client', { 21 | iceServers: [], 22 | }); 23 | 24 | const echoStream = new nodeDataChannel.DataChannelStream( 25 | echoPeer.createDataChannel('echo-channel'), 26 | ); 27 | echoStream.pipe(echoStream); // Echo all received data back to the client 28 | 29 | await waitForGathering(echoPeer); 30 | 31 | const { sdp: echoDescSdp, type: echoDescType } = echoPeer.localDescription(); 32 | clientPeer.setRemoteDescription(echoDescSdp, echoDescType); 33 | await waitForGathering(clientPeer); 34 | 35 | const { sdp: clientDescSdp, type: clientDescType } = clientPeer.localDescription(); 36 | echoPeer.setRemoteDescription(clientDescSdp, clientDescType); 37 | 38 | const clientChannel: nodeDataChannel.DataChannel = await new Promise((resolve) => 39 | clientPeer.onDataChannel(resolve), 40 | ); 41 | 42 | const clientResponsePromise = new Promise((resolve) => clientChannel.onMessage(resolve)); 43 | clientChannel.sendMessage('test message'); 44 | 45 | expect(await clientResponsePromise).toBe('test message'); 46 | 47 | clientChannel.close(); 48 | clientPeer.close(); 49 | echoPeer.close(); 50 | }); 51 | }); 52 | 53 | afterAll(() => { 54 | // Properly cleanup so Jest does not complain about asynchronous operations that weren't stopped. 55 | nodeDataChannel.cleanup(); 56 | }); 57 | -------------------------------------------------------------------------------- /test/jest-tests/websocket.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import * as nodeDataChannel from '../../src/lib/index'; 3 | 4 | describe('Websocket', () => { 5 | const webSocketServer = new nodeDataChannel.WebSocketServer({ 6 | bindAddress: '127.0.0.1', 7 | port: 1987, 8 | }); 9 | const clientSocket = new nodeDataChannel.WebSocket(); 10 | 11 | // Mocks 12 | const clientOnOpenMock = jest.fn(); 13 | const clientOnMessageMock = jest.fn(); 14 | const clientOnCLosedMock = jest.fn(); 15 | 16 | const webServerOnClientMock = jest.fn(); 17 | const webServerOnOpenMock = jest.fn(); 18 | const webServerOnMessageMock = jest.fn(); 19 | const webServerOnClosedMock = jest.fn(); 20 | 21 | webSocketServer.onClient((serverSocket) => { 22 | webServerOnClientMock(); 23 | 24 | serverSocket.onOpen(() => { 25 | webServerOnOpenMock(); 26 | }); 27 | 28 | serverSocket.onMessage((message) => { 29 | webServerOnMessageMock(message); 30 | serverSocket.sendMessage('reply to ' + message); 31 | serverSocket.close(); 32 | }); 33 | 34 | serverSocket.onClosed(() => { 35 | webServerOnClosedMock(); 36 | serverSocket.close(); 37 | }); 38 | }); 39 | 40 | test('can create a websocket client and connect to a server', () => { 41 | return new Promise((done) => { 42 | clientSocket.open('ws://127.0.0.1:1987'); 43 | clientSocket.onOpen(() => { 44 | clientOnOpenMock(); 45 | clientSocket.sendMessage('Hello'); 46 | }); 47 | 48 | clientSocket.onMessage((message) => { 49 | clientOnMessageMock(message); 50 | }); 51 | 52 | clientSocket.onClosed(() => { 53 | clientOnCLosedMock(); 54 | }); 55 | 56 | setTimeout(() => { 57 | expect(clientOnMessageMock.mock.calls[0][0]).toEqual('reply to Hello'); 58 | expect(clientOnOpenMock.mock.calls.length).toBe(1); 59 | expect(clientOnMessageMock.mock.calls.length).toBe(1); 60 | expect(clientOnCLosedMock.mock.calls.length).toBe(1); 61 | 62 | expect(webServerOnMessageMock.mock.calls[0][0]).toEqual('Hello'); 63 | expect(webServerOnOpenMock.mock.calls.length).toBe(1); 64 | expect(webServerOnMessageMock.mock.calls.length).toBe(1); 65 | expect(webServerOnClosedMock.mock.calls.length).toBe(1); 66 | 67 | clientSocket.close(); 68 | webSocketServer.stop(); 69 | 70 | done(); 71 | }, 3000); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/leak-test/index.js: -------------------------------------------------------------------------------- 1 | // https://github.com/murat-dogan/node-datachannel/issues/349 2 | // from https://github.com/achingbrain 3 | 4 | import why from 'why-is-node-running'; 5 | import { RTCPeerConnection } from 'node-datachannel/polyfill'; 6 | import { initLogger } from 'node-datachannel'; 7 | 8 | initLogger('Error'); 9 | 10 | process.stdout.write('.'); 11 | 12 | // how many channels to open 13 | const numChannels = 20; 14 | 15 | // how much data to send on each one 16 | const send = 1024 * 1024; 17 | 18 | // the chunk size to send - needs to divide `send` with no remainder 19 | const chunkSize = 1024; 20 | 21 | const peer1 = new RTCPeerConnection(); 22 | const peer2 = new RTCPeerConnection(); 23 | 24 | // track channel status 25 | const channelStatus = {}; 26 | 27 | // echo any data back to the sender 28 | peer2.addEventListener('datachannel', (evt) => { 29 | const channel = evt.channel; 30 | const label = channel.label; 31 | 32 | channelStatus[`incoming-${label}`] = 'open'; 33 | 34 | channel.addEventListener('message', (evt) => { 35 | channel.send(evt.data); 36 | }); 37 | channel.addEventListener('close', (evt) => { 38 | console.info(`channel closed: ${label}`); 39 | delete channelStatus[`incoming-${label}`]; 40 | }); 41 | }); 42 | 43 | const channels = []; 44 | 45 | // create channels 46 | for (let i = 0; i < numChannels; i++) { 47 | channels.push(peer1.createDataChannel(`c-${i}`)); 48 | } 49 | 50 | // ensure peers are connected 51 | await connectPeers(peer1, peer2); 52 | 53 | // send data over each channel in parallel 54 | await Promise.all( 55 | channels.map(async (channel) => { 56 | channel.binaryType = 'arraybuffer'; 57 | const label = channel.label; 58 | 59 | await isOpen(channel); 60 | 61 | channelStatus[`outgoing-${label}`] = 'open'; 62 | 63 | // send data and wait for it to be echoed back 64 | return new Promise((resolve, reject) => { 65 | let received = 0; 66 | let sent = 0; 67 | 68 | channel.addEventListener('message', (evt) => { 69 | received += evt.data.byteLength; 70 | 71 | // all data has been echoed back to us 72 | if (received === send) { 73 | // this makes no difference 74 | // channel.close(); 75 | resolve(); 76 | } 77 | }); 78 | 79 | channel.addEventListener('close', (evt) => { 80 | delete channelStatus[`outgoing-${label}`]; 81 | }); 82 | 83 | while (sent !== send) { 84 | channel.send(new Uint8Array(chunkSize)); 85 | sent += chunkSize; 86 | } 87 | }); 88 | }), 89 | ); 90 | 91 | // close connections 92 | peer1.close(); 93 | peer2.close(); 94 | 95 | // print open handles after 5s - unref so this timeout doesn't keep the event loop running 96 | setTimeout(() => { 97 | console.info('\n-- channels'); 98 | console.info(JSON.stringify(channelStatus, null, 2)); 99 | console.info(''); 100 | 101 | why(); 102 | }, 5_000).unref(); 103 | 104 | export async function connectPeers(peer1, peer2) { 105 | const peer1Offer = await peer1.createOffer(); 106 | await peer2.setRemoteDescription(peer1Offer); 107 | 108 | const peer2Answer = await peer2.createAnswer(); 109 | await peer1.setRemoteDescription(peer2Answer); 110 | 111 | peer1.addEventListener('icecandidate', (e) => { 112 | peer2.addIceCandidate(e.candidate); 113 | }); 114 | 115 | peer2.addEventListener('icecandidate', (e) => { 116 | peer1.addIceCandidate(e.candidate); 117 | }); 118 | 119 | await Promise.all([isConnected(peer1), isConnected(peer2)]); 120 | } 121 | 122 | async function isConnected(peer) { 123 | return new Promise((resolve, reject) => { 124 | peer.addEventListener('connectionstatechange', () => { 125 | if (peer.connectionState === 'connected') { 126 | resolve(); 127 | } 128 | 129 | if (peer.connectionState === 'failed') { 130 | reject(new Error('Connection failed')); 131 | } 132 | }); 133 | }); 134 | } 135 | 136 | async function isOpen(dc) { 137 | if (dc.readyState === 'open') { 138 | return; 139 | } 140 | 141 | return new Promise((resolve, reject) => { 142 | dc.addEventListener('open', () => { 143 | resolve(); 144 | }); 145 | dc.addEventListener('error', () => { 146 | reject(new Error('Channel error')); 147 | }); 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /test/leak-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leak-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-datachannel": "file:../..", 15 | "until-death": "^1.0.0", 16 | "why-is-node-running": "^3.2.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/polyfill-connectivity.ts: -------------------------------------------------------------------------------- 1 | import { RTCPeerConnection, RTCDataChannel } from '../src/polyfill/index'; 2 | import nodeDataChannel from '../src/lib/index'; 3 | 4 | nodeDataChannel.initLogger('Info'); 5 | 6 | let dc1: RTCDataChannel = null; 7 | let dc2: RTCDataChannel = null; 8 | 9 | const peer1 = new RTCPeerConnection({ 10 | peerIdentity: 'peer1', 11 | iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], 12 | }); 13 | 14 | // Set Callbacks 15 | peer1.onconnectionstatechange = (): void => { 16 | console.log('Peer1 State:', peer1.connectionState); 17 | }; 18 | peer1.oniceconnectionstatechange = (): void => { 19 | console.log('Peer1 IceState:', peer1.iceConnectionState); 20 | }; 21 | peer1.onicegatheringstatechange = (): void => { 22 | console.log('Peer1 GatheringState:', peer1.iceGatheringState); 23 | }; 24 | peer1.onicecandidate = (e): void => { 25 | console.log('Peer1 Candidate:', e.candidate.candidate); 26 | peer2.addIceCandidate(e.candidate); 27 | }; 28 | 29 | const peer2 = new RTCPeerConnection({ 30 | peerIdentity: 'peer2', 31 | iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], 32 | }); 33 | 34 | // Set Callbacks 35 | peer2.onconnectionstatechange = (): void => { 36 | console.log('Peer2 State:', peer2.connectionState); 37 | }; 38 | peer2.oniceconnectionstatechange = (): void => { 39 | console.log('Peer2 IceState:', peer2.iceConnectionState); 40 | }; 41 | peer2.onicegatheringstatechange = (): void => { 42 | console.log('Peer2 GatheringState:', peer2.iceGatheringState); 43 | }; 44 | peer2.onicecandidate = (e): void => { 45 | console.log('Peer2 Candidate:', e.candidate.candidate); 46 | peer1.addIceCandidate(e.candidate); 47 | }; 48 | peer2.ondatachannel = (dce): void => { 49 | console.log('Peer2 Got DataChannel: ', dce.channel.label); 50 | dc2 = dce.channel; 51 | dc2.onmessage = (msg): void => { 52 | console.log('Peer2 Received Msg:', msg.data.toString()); 53 | }; 54 | dc2.send('Hello From Peer2'); 55 | dc2.onclose = (): void => { 56 | console.log('dc2 closed'); 57 | }; 58 | }; 59 | 60 | dc1 = peer1.createDataChannel('test'); 61 | dc1.onopen = (): void => { 62 | dc1.send('Hello from Peer1'); 63 | }; 64 | dc1.onmessage = (msg): void => { 65 | console.log('Peer1 Received Msg:', msg.data.toString()); 66 | }; 67 | dc1.onclose = (): void => { 68 | console.log('dc1 closed'); 69 | }; 70 | 71 | peer1 72 | .createOffer() 73 | .then((desc) => { 74 | peer2.setRemoteDescription(desc); 75 | }) 76 | .catch((err) => console.error(err)); 77 | 78 | peer2 79 | .createAnswer() 80 | .then((answerDesc) => { 81 | peer1.setRemoteDescription(answerDesc); 82 | }) 83 | .catch((err) => console.error("Couldn't create answer", err)); 84 | 85 | setTimeout(() => { 86 | peer1.close(); 87 | peer2.close(); 88 | }, 5 * 1000); 89 | -------------------------------------------------------------------------------- /test/websockets.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, initLogger, preload, WebSocket, WebSocketServer } from '../src/lib'; 2 | 3 | initLogger('Debug'); 4 | preload(); 5 | 6 | const webSocketServer = new WebSocketServer({ 7 | bindAddress: '127.0.0.1', 8 | port: 1987, 9 | }); 10 | 11 | webSocketServer.onClient((serverSocket) => { 12 | console.log( 13 | 'webSocketServer.onClient() remoteAddress: ' + 14 | serverSocket.remoteAddress() + 15 | ', path: ' + 16 | serverSocket.path(), 17 | ); 18 | 19 | serverSocket.onOpen(() => { 20 | console.log('serverSocket.onOpen()'); 21 | }); 22 | 23 | serverSocket.onMessage((message) => { 24 | console.log('serverSocket.onMessage():', message); 25 | serverSocket.sendMessage('reply to ' + message); 26 | }); 27 | 28 | serverSocket.onClosed(() => { 29 | console.log('serverSocket.onClosed()'); 30 | serverSocket.close(); 31 | }); 32 | }); 33 | 34 | const clientSocket = new WebSocket(); 35 | 36 | clientSocket.onOpen(() => { 37 | console.log('clientSocket.onOpen()'); 38 | clientSocket.sendMessage('Hello'); 39 | }); 40 | 41 | clientSocket.onMessage((message) => { 42 | console.log('clientSocket.onMessage():', message); 43 | clientSocket.forceClose(); 44 | webSocketServer.stop(); 45 | }); 46 | 47 | clientSocket.onClosed(() => { 48 | console.log('clientSocket.onClosed()'); 49 | clientSocket.close(); 50 | }); 51 | 52 | clientSocket.open('ws://127.0.0.1:1987'); 53 | 54 | setTimeout(() => { 55 | cleanup(); 56 | }, 1000); 57 | -------------------------------------------------------------------------------- /test/wpt-tests/README.md: -------------------------------------------------------------------------------- 1 | # Setup (First time usage) 2 | 3 | > cd test/wpt-tests 4 | 5 | > npm i 6 | 7 | > git submodule update --init --recursive --depth 1 8 | 9 | > cd wpt 10 | 11 | > ./wpt make-hosts-file | sudo tee -a /etc/hosts 12 | 13 | After your hosts file is configured, the servers will be locally accessible at: 14 | 15 | http://web-platform.test:8000/ 16 | 17 | https://web-platform.test:8443/ \* 18 | 19 | To use the web-based runner point your browser to: 20 | 21 | http://web-platform.test:8000/tools/runner/index.html 22 | 23 | https://web-platform.test:8443/tools/runner/index.html \* 24 | 25 | # Running Tests 26 | 27 | > npm run test 28 | 29 | OR 30 | 31 | > npm run wpt:server (Let this run) 32 | 33 | > npm run wpt:test (In an other console) 34 | 35 | The files that tested are listed in `wpt-test-list.js`. The commented lines need still to be one-by-one tested and uncommented. 36 | 37 | Contributions are welcome! 38 | 39 | # Run a test for Chrome 40 | 41 | ./wpt run chrome /webrtc/RTCPeerConnection-addIceCandidate.html 42 | 43 | # Latest Test Results 44 | 45 | Please check [last-test-results.md](./last-test-results.md) page 46 | -------------------------------------------------------------------------------- /test/wpt-tests/chrome-failed-tests.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { runWptTests, WptTestResult } from './wpt'; 4 | 5 | // Some tests also fail in Chrome 6 | // We don't also care about them 7 | let chromeFailedTests: WptTestResult[] = []; 8 | let totalNumberOfTests = 0; 9 | 10 | export async function runChromeTests(wptTestList: string[]): Promise { 11 | chromeFailedTests = []; 12 | totalNumberOfTests = 0; 13 | if (!process.env.NO_CACHE_FOR_CHROME_TESTS) { 14 | console.log('Default is to read chromeFailedTests from json file'); 15 | console.log('While it takes time to run all tests'); 16 | console.log('Set NO_CACHE_FOR_CHROME_TESTS to true in order to run all tests from scratch'); 17 | const filePath = path.join(__dirname, 'chromeFailedTests.json'); 18 | if (fs.existsSync(filePath)) { 19 | const chromeFailedTestsFromFile = JSON.parse(fs.readFileSync(filePath).toString()); 20 | // Filter out tests that are not in wptTestList 21 | chromeFailedTests = chromeFailedTestsFromFile.filter((test: WptTestResult) => 22 | wptTestList.includes(test.test), 23 | ); 24 | for (let i = 0; i < chromeFailedTests.length; i++) { 25 | totalNumberOfTests += chromeFailedTests[i].result.length; 26 | } 27 | } 28 | return; 29 | } 30 | 31 | const results = await runWptTests(wptTestList, true); 32 | for (let i = 0; i < results.length; i++) { 33 | totalNumberOfTests += results[i].result?.length || 0; 34 | if (results[i].result && results[i].result.some((test) => test.status === 1)) { 35 | chromeFailedTests.push({ 36 | test: results[i].test, 37 | result: results[i].result.filter((test) => test.status === 1), 38 | }); 39 | } 40 | } 41 | 42 | // Write chromeFailedTests to json file 43 | const filePath = path.join(__dirname, 'chromeFailedTests.json'); 44 | fs.writeFileSync(filePath, JSON.stringify(chromeFailedTests, null, 2)); 45 | } 46 | 47 | export function getChromeFailedTests(): WptTestResult[] { 48 | return chromeFailedTests; 49 | } 50 | 51 | export function isTestForChromeFailed(testPath, testName): boolean { 52 | return chromeFailedTests.some( 53 | (test) => 54 | test.test === testPath && 55 | test.result.some((result) => result.name === testName && result.status === 1), 56 | ); 57 | } 58 | 59 | export function getTotalNumberOfTests(): number { 60 | return totalNumberOfTests; 61 | } 62 | 63 | // Test 64 | // (async () => { 65 | // await runChromeTests(); 66 | // console.log(getChromeFailedTests()); 67 | // })(); 68 | -------------------------------------------------------------------------------- /test/wpt-tests/index.ts: -------------------------------------------------------------------------------- 1 | import ndc from '../../src/lib/index'; 2 | import wptTestList from './wpt-test-list'; 3 | import { runWptTests, WptTestResult } from './wpt'; 4 | import { runChromeTests, isTestForChromeFailed } from './chrome-failed-tests'; 5 | 6 | // Set the log level, for debugging purposes 7 | // ndc.initLogger('Debug'); 8 | 9 | // Catch unhandled exceptions 10 | process.on('unhandledRejection', (reason, promise) => { 11 | console.error('Unhandled Rejection at:', promise, 'reason:', reason); 12 | process.exit(1); 13 | }); 14 | 15 | async function run(): Promise { 16 | // Run tests for Chrome 17 | console.log('# Running tests for Chrome...'); 18 | await runChromeTests(wptTestList); 19 | 20 | // Run tests for node-datachannel 21 | console.log(''); 22 | console.log('# Running tests for node-datachannel...'); 23 | const results: WptTestResult[] = await runWptTests(wptTestList); 24 | 25 | // Calc total number of tests 26 | let totalTests = 0; 27 | results.forEach((result) => { 28 | totalTests += result.result.length; 29 | }); 30 | 31 | // Compare results 32 | // Filter failed tests for node-datachannel 33 | const failedTestsLibrary: WptTestResult[] = []; 34 | results.forEach((result) => { 35 | if (result.result.some((test) => test.status === 1)) { 36 | failedTestsLibrary.push({ 37 | test: result.test, 38 | result: result.result.filter((test) => test.status === 1), 39 | }); 40 | } 41 | }); 42 | let totalFailedTestsLibrary = 0; 43 | failedTestsLibrary.forEach((result) => { 44 | totalFailedTestsLibrary += result.result.length; 45 | }); 46 | 47 | // Filter out any failed tests that also failed in Chrome 48 | const failedTests: WptTestResult[] = []; 49 | failedTestsLibrary.forEach((result) => { 50 | if (result.result.some((test) => !isTestForChromeFailed(result.test, test.name))) { 51 | failedTests.push({ 52 | test: result.test, 53 | result: result.result.filter((test) => !isTestForChromeFailed(result.test, test.name)), 54 | }); 55 | } 56 | }); 57 | let totalFailedTests = 0; 58 | failedTests.forEach((result) => { 59 | totalFailedTests += result.result.length; 60 | }); 61 | // console.log(JSON.stringify(failedTests, null, 2)); 62 | 63 | // Print Report 64 | // Print Test Names 65 | console.log(''); 66 | console.log('# Tests Report'); 67 | // Total number of tests 68 | console.log('Total Tests [Library]: ', totalTests); 69 | // Number of passed tests 70 | console.log('Passed Tests: ', totalTests - totalFailedTestsLibrary, ' '); 71 | // Number of failed tests for chrome + node-datachannel 72 | console.log( 73 | 'Failed Tests (Chrome + Library): ', 74 | totalFailedTestsLibrary - totalFailedTests, 75 | " (We don't care about these tests) ", 76 | ); 77 | // Number of failed tests 78 | console.log('Failed Tests: ', totalFailedTests, ' '); 79 | 80 | // Print Failed Tests 81 | console.log(''); 82 | console.log('## Failed Tests'); 83 | for (let i = 0; i < failedTests.length; i++) { 84 | console.log(`### ${failedTests[i].test}`); 85 | for (let j = 0; j < failedTests[i].result.length; j++) { 86 | console.log(`- name: ${failedTests[i].result[j].name} `); 87 | console.log(` message: ${failedTests[i].result[j].message} `); 88 | } 89 | } 90 | 91 | // Sometimes failed tests are not cleaned up 92 | // This can prevent the process from exiting 93 | ndc.cleanup(); 94 | console.log('End of tests'); 95 | } 96 | 97 | run().catch((error) => { 98 | console.error(error); 99 | process.exit(1); 100 | }); 101 | -------------------------------------------------------------------------------- /test/wpt-tests/wpt.ts: -------------------------------------------------------------------------------- 1 | // Run WPT manually before calling this script 2 | 3 | import { JSDOM, VirtualConsole } from 'jsdom'; 4 | import { TextEncoder, TextDecoder } from 'util'; 5 | import puppeteer, { Browser } from 'puppeteer'; 6 | import ndcPolyfill from '../../src/polyfill/index'; 7 | 8 | export interface TestResult { 9 | name: string; 10 | message: string; 11 | status: number; // 0: PASS, 1: FAIL, 2: TIMEOUT, 3: PRECONDITION_FAILED 12 | } 13 | 14 | export interface WptTestResult { 15 | test: string; 16 | result: TestResult[]; 17 | } 18 | 19 | export async function runWptTests( 20 | wptTestList: string[], 21 | _forChrome = false, 22 | _wptServerUrl = 'http://web-platform.test:8000', 23 | ): Promise { 24 | let browser: Browser = null; 25 | const results: WptTestResult[] = []; 26 | 27 | if (_forChrome) 28 | browser = await puppeteer.launch({ 29 | headless: true, 30 | devtools: true, 31 | }); 32 | 33 | // call runTest for each test path 34 | for (let i = 0; i < wptTestList.length; i++) { 35 | console.log(`Running test: ${wptTestList[i]} `); 36 | const path = `${_wptServerUrl}${wptTestList[i]}`; 37 | const result: TestResult[] = _forChrome 38 | ? await runTestForChrome(browser, path) 39 | : await runTestForLibrary(path); 40 | results.push({ test: wptTestList[i], result }); 41 | 42 | // sleep for 1 second 43 | // await new Promise((resolve) => setTimeout(resolve, 1000)); 44 | } 45 | 46 | // close the client 47 | if (_forChrome) await browser.close(); 48 | 49 | return results; 50 | } 51 | 52 | function runTestForLibrary(filePath: string): Promise { 53 | // return new promise 54 | return new Promise((resolve) => { 55 | const virtualConsole = new VirtualConsole(); 56 | virtualConsole.sendTo(console); 57 | 58 | JSDOM.fromURL(filePath, { 59 | runScripts: 'dangerously', 60 | resources: 'usable', 61 | pretendToBeVisual: true, 62 | virtualConsole, 63 | beforeParse(window: any) { 64 | // Assign the polyfill to the window object 65 | Object.assign(window, ndcPolyfill); 66 | 67 | // Overwrite the DOMException object 68 | window.DOMException = DOMException; 69 | window.TypeError = TypeError; 70 | window.TextEncoder = TextEncoder; 71 | window.TextDecoder = TextDecoder; 72 | window.Uint8Array = Uint8Array; 73 | window.ArrayBuffer = ArrayBuffer; 74 | }, 75 | }).then((dom: any) => { 76 | // Get the window object from the DOM 77 | const { window } = dom; 78 | window.addEventListener('load', () => { 79 | window.add_completion_callback((results) => { 80 | window.close(); 81 | 82 | // Meaning of status 83 | // 0: PASS (test passed) 84 | // 1: FAIL (test failed) 85 | // 2: TIMEOUT (test timed out) 86 | // 3: PRECONDITION_FAILED (test skipped) 87 | const returnObject = []; 88 | for (let i = 0; i < results.length; i++) { 89 | returnObject.push({ 90 | name: results[i].name, 91 | message: results[i].message, 92 | status: results[i].status, 93 | }); 94 | } 95 | return resolve(returnObject); 96 | }); 97 | }); 98 | }); 99 | }); 100 | } 101 | 102 | async function runTestForChrome(browser: Browser, filePath: string): Promise { 103 | const page = await browser.newPage(); 104 | // Evaluate the script in the page context 105 | await page.evaluateOnNewDocument(() => { 106 | function createDeferredPromise(): Promise { 107 | let resolve: any, reject: any; 108 | 109 | const promise = new Promise(function (_resolve, _reject) { 110 | resolve = _resolve; 111 | reject = _reject; 112 | }); 113 | 114 | (promise as any).resolve = resolve; 115 | (promise as any).reject = reject; 116 | return promise; 117 | } 118 | 119 | window.addEventListener('load', () => { 120 | (window as any).resultPromise = createDeferredPromise(); 121 | (window as any).add_completion_callback((results) => { 122 | // window.returnTestResults.push({ name: test.name, message: test.message, status: test.status }); 123 | const returnTestResults = []; 124 | for (let i = 0; i < results.length; i++) { 125 | returnTestResults.push({ 126 | name: results[i].name, 127 | message: results[i].message, 128 | status: results[i].status, 129 | }); 130 | } 131 | (window as any).resultPromise.resolve(returnTestResults); 132 | }); 133 | }); 134 | }); 135 | 136 | // Navigate to the specified URL 137 | await page.goto(filePath, { waitUntil: 'load' }); 138 | 139 | // get the results 140 | const results = await page.evaluate(() => { 141 | return (window as any).resultPromise; 142 | }); 143 | 144 | // close the page 145 | await page.close(); 146 | 147 | return results; 148 | } 149 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleResolution": "Node", 5 | "rootDir": ".", 6 | "importsNotUsedAsValues": "remove", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noImplicitAny": false, 11 | "noImplicitThis": false, 12 | "noImplicitReturns": true, 13 | "strictNullChecks": false, 14 | "noUnusedLocals": true, 15 | "alwaysStrict": true, 16 | "outDir": "./dist", 17 | "module": "CommonJS" 18 | }, 19 | "include": ["src/**/*", "test/**/*"], 20 | "exclude": ["node_modules", "dist", "src/cpp", "test/wpt-tests/wpt"] 21 | } 22 | --------------------------------------------------------------------------------