├── .eslintrc.js
├── .github
└── workflows
│ ├── deploy_backend.yml
│ ├── deploy_frontend.yml
│ └── release.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .lintstagedrc.js
├── .prettierrc.json
├── .tool-versions
├── .vscode
└── settings.json
├── README.md
├── commitlint.config.js
├── package.json
├── packages
├── backend
│ ├── Procfile
│ ├── https
│ │ ├── domain.crt
│ │ ├── domain.csr
│ │ └── domain.key
│ ├── index.js
│ └── package.json
└── frontend
│ ├── .gitignore
│ ├── components
│ ├── errorModal.jsx
│ ├── externalLink.jsx
│ ├── footer.jsx
│ ├── localVideos.jsx
│ ├── modal.jsx
│ ├── navbar.jsx
│ ├── remoteStreams.jsx
│ ├── remoteVideos.jsx
│ ├── roomDetails.jsx
│ ├── userDetails.jsx
│ ├── video.jsx
│ └── vlogVideo.jsx
│ ├── hooks
│ ├── pageVisibility.js
│ ├── socketConnection.js
│ └── useGetDevices.js
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ ├── [roomName].jsx
│ ├── _app.jsx
│ ├── api
│ │ └── hello.js
│ ├── index.jsx
│ └── meeting.jsx
│ ├── postcss.config.js
│ ├── public
│ ├── 114x114.png
│ ├── 120x120.png
│ ├── 128x128.png
│ ├── 144x144.png
│ ├── 152x152.png
│ ├── 180x180.png
│ ├── 57x57-no-bg.png
│ ├── 57x57.png
│ ├── 72x72.png
│ ├── 76x76.png
│ ├── brand-1200x600.png
│ ├── brand-192x192.png
│ ├── brand-200x200.png
│ ├── brand-430x495.png
│ ├── brand-512x512.png
│ ├── brand-800x800.png
│ ├── favicon.ico
│ ├── robots.txt
│ ├── sitemap.xml
│ └── virtual-bgs
│ │ └── 1.jpg
│ ├── sitemap-generator.js
│ ├── store
│ └── states.js
│ ├── styles
│ └── globals.scss
│ ├── tailwind.config.js
│ └── utils
│ ├── classNames.js
│ ├── constants.js
│ └── helpers.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "plugin:react/recommended",
4 | "eslint:recommended",
5 | "plugin:import/errors",
6 | "plugin:import/warnings",
7 | "react-app",
8 | ],
9 | env: {
10 | browser: true,
11 | commonjs: true,
12 | es6: true,
13 | jest: true,
14 | node: true,
15 | },
16 | settings: {
17 | react: {
18 | version: "detect",
19 | },
20 | "import/resolver": {
21 | node: {
22 | extensions: [".js", ".jsx"],
23 | },
24 | },
25 | },
26 | parser: "babel-eslint",
27 | parserOptions: {
28 | ecmaVersion: 2017,
29 | sourceType: "module",
30 | ecmaFeatures: {
31 | jsx: true,
32 | },
33 | },
34 | rules: {
35 | "no-alert": 0,
36 | "no-console": ["error", { allow: ["error", "info", "debug"] }],
37 | "react/prop-types": 0,
38 | "react/sort-prop-types": 0,
39 | "import/order": 0,
40 | "react-hooks/rules-of-hooks": "error",
41 | "react-hooks/exhaustive-deps": "warn",
42 | "react/no-children-prop": "error",
43 | "react/jsx-no-target-blank": "error",
44 | "react/jsx-key": "error",
45 | "react/react-in-jsx-scope": "off",
46 | "react/jsx-uses-react": "off",
47 | "react/jsx-tag-spacing": ["error"],
48 | "react/jsx-filename-extension": ["error"],
49 | "no-useless-constructor": ["error"],
50 | eqeqeq: ["error"],
51 | "default-case": "off",
52 | "jsx-a11y/anchor-is-valid": "error",
53 | "jsx-a11y/img-redundant-alt": "error",
54 | "jsx-a11y/alt-text": "error",
55 | "no-useless-concat": "error",
56 | "no-unused-vars": "error",
57 | "no-multiple-empty-lines": [
58 | "error",
59 | {
60 | max: 1,
61 | maxEOF: 1,
62 | maxBOF: 0,
63 | },
64 | ],
65 | camelcase: 0,
66 | semi: ["error", "always"],
67 | },
68 | globals: {
69 | globalThis: true,
70 | bodyPix: true,
71 | },
72 | plugins: ["react", "react-hooks", "jsx-a11y", "import"],
73 | overrides: [
74 | {
75 | files: ["webpack/build.js", "webpack/start.js"],
76 | rules: {
77 | "no-console": "off",
78 | },
79 | },
80 | ],
81 | ignorePatterns: ["stories/**/*", "node_modules/**/*", "build/**/*", "canvasjs.min.jsx"],
82 | };
83 |
--------------------------------------------------------------------------------
/.github/workflows/deploy_backend.yml:
--------------------------------------------------------------------------------
1 | name: deploy_backend
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - declarative-rtc
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout source code
13 | uses: actions/checkout@v1
14 |
15 | - name: Add remote origin
16 | run: git remote add heroku https://heroku:${{ secrets.HEROKU_API_KEY }}@git.heroku.com/webrtc-next-demo.git
17 |
18 | - name: Deploy backend to heroku
19 | run: git push --force heroku `git subtree split --prefix packages/backend HEAD`:refs/heads/master
20 |
--------------------------------------------------------------------------------
/.github/workflows/deploy_frontend.yml:
--------------------------------------------------------------------------------
1 | name: deploy_frontend
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - declarative-rtc
8 |
9 | jobs:
10 | build:
11 | name: Deploy to openrtc.vercel.app
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - name: Setup Node
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: "14.x"
21 |
22 | - name: Install
23 | run: |
24 | yarn --frozen-lockfile
25 |
26 | - name: Deploy
27 | uses: amondnet/vercel-action@v20
28 | with:
29 | vercel-token: ${{ secrets.VERCEL_TOKEN }}
30 | vercel-args: "--prod"
31 | vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
32 | vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
33 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 |
7 | jobs:
8 | changelog:
9 | runs-on: ubuntu-latest
10 | name: create release on tag
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | # This action generates changelog which then the release action consumes
16 | - name: Conventional Changelog Action
17 | id: changelog
18 | uses: TriPSs/conventional-changelog-action@v3
19 | with:
20 | github-token: ${{ secrets.github_token }}
21 | skip-commit: 'true'
22 |
23 | - name: Create Release
24 | uses: actions/create-release@v1
25 | if: ${{ steps.changelog.outputs.skipped == 'false' }}
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.github_token }}
28 | with:
29 | tag_name: ${{ steps.changelog.outputs.tag }}
30 | release_name: ${{ steps.changelog.outputs.tag }}
31 | body: ${{ steps.changelog.outputs.clean_changelog }}
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | **/node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | **/.next/
13 | **/out/
14 |
15 | # production
16 | **/build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # Created by https://www.toptal.com/developers/gitignore/api/macos
38 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos
39 |
40 | ### macOS ###
41 | # General
42 | .DS_Store
43 | .AppleDouble
44 | .LSOverride
45 |
46 | # Icon must end with two \r
47 | Icon
48 |
49 |
50 | # Thumbnails
51 | ._*
52 |
53 | # Files that might appear in the root of a volume
54 | .DocumentRevisions-V100
55 | .fseventsd
56 | .Spotlight-V100
57 | .TemporaryItems
58 | .Trashes
59 | .VolumeIcon.icns
60 | .com.apple.timemachine.donotpresent
61 |
62 | # Directories potentially created on remote AFP share
63 | .AppleDB
64 | .AppleDesktop
65 | Network Trash Folder
66 | Temporary Items
67 | .apdisk
68 |
69 | # End of https://www.toptal.com/developers/gitignore/api/macos
70 |
71 | # Created by https://www.toptal.com/developers/gitignore/api/vim
72 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim
73 |
74 | ### Vim ###
75 | # Swap
76 | [._]*.s[a-v][a-z]
77 | !*.svg # comment out if you don't need vector files
78 | [._]*.sw[a-p]
79 | [._]s[a-rt-v][a-z]
80 | [._]ss[a-gi-z]
81 | [._]sw[a-p]
82 |
83 | # Session
84 | Session.vim
85 | Sessionx.vim
86 |
87 | # Temporary
88 | .netrwhist
89 | *~
90 | # Auto-generated tag files
91 | tags
92 | # Persistent undo
93 | [._]*.un~
94 |
95 | # End of https://www.toptal.com/developers/gitignore/api/vim
96 |
97 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit ""
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install lint-staged -p true
5 |
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "**/*.js?(x)": (filenames) => {
3 | let concatedFileNames = filenames
4 | .map((filename) => `"${filename}"`)
5 | .join(" ");
6 | return [
7 | `prettier --write ${concatedFileNames}`,
8 | `eslint --fix ${concatedFileNames}`,
9 | ];
10 | },
11 | "**/*.js": (filenames) => {
12 | let concatedFileNames = filenames
13 | .map((filename) => `"${filename}"`)
14 | .join(" ");
15 | return [
16 | `prettier --write ${concatedFileNames}`,
17 | `eslint --fix ${concatedFileNames}`,
18 | ];
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120
3 | }
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 15.8.0
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "liveFrame.url": "http://localhost:3000",
3 | "liveFrame.title": "WebRTC"
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # An open source live screen share and webcam video recorder
2 |
3 | - [An open source live screen share and webcam video recorder](#an-open-source-live-screen-share-and-webcam-video-recorder)
4 | - [Deploy your instance](#deploy-your-instance)
5 | - [Features](#features)
6 | - [Contributions](#contributions)
7 | - [Requirements](#requirements)
8 | - [Code structure](#code-structure)
9 | - [How to run](#how-to-run)
10 | - [.env contents](#env-contents)
11 | - [How to build](#how-to-build)
12 | - [TO DO](#to-do)
13 |
14 | ## Deploy your instance
15 |
16 | 1. Deploy the backend project to some cloud (heroku etc) to consume websockets.
17 | 1. Deploy the frontend project to vercel with single click [](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Ftechnikhil314%2Fnext-webrtc)
18 | 1. Add required env variables
19 | - NEXT_PUBLIC_WEBSOCKET_URL - URL to your websocket server. The backend deployment. If you want to use meeting feature
20 | - NEXT_PUBLIC_URL - URL of your site
21 | - NODE_ENV - `development` or `production`
22 |
23 | ## Features
24 |
25 | - Background removal
26 | - Virtual backgrounds
27 | - Background blur
28 |
29 | ## Contributions
30 |
31 | Welcome :)
32 |
33 | ### Requirements
34 |
35 | 1. Node >= v15.x
36 | 2. npm >= v7
37 | 3. yarn 1.x
38 |
39 | ### Code structure
40 |
41 | 1. It is a monorepo managed by yarn
42 | 2. There are two packages backend and frontend
43 | 3. Backend is just a small websocket server used for signaling
44 | 4. Frontend is actual UI built with nextjs
45 | 5. It uses google stun server for populating ice candidates
46 |
47 | ### How to run
48 |
49 | 1. Install all dependencies using `yarn --frozen-lockfile`
50 | 2. add `.env` see the contents [below](#env-contents)
51 | 3. run backend using `yarn workspaces @openrtc/backend start`
52 | 4. run frontend using `yarn workspaces @openrtc/frontend start`
53 |
54 | ### .env contents
55 |
56 | ```bash
57 | NEXT_PUBLIC_WEBSOCKET_URL=wss://localhost:4000/
58 | NODE_ENV=development
59 | NEXT_PUBLIC_URL=http://localhost:3000
60 | ```
61 |
62 | ### How to build
63 |
64 | 1. You dont need to build backend
65 | 1. You can build frontend package with `yarn workspaces @openrtc/frontend build` command
66 |
67 | ## TO DO
68 |
69 | 1. Handle if someone stops screen share in recording vlog
70 | - This currently stops recording
71 | - Ideally it should keep on recording but change video from screen to user
72 | 1. First time visitor faces lag in audio recording
73 | 1. First time visitor can not see small PiP video
74 | 1. Microphone volume control
75 | 1. Ability to name the video file - Currently it uses ISO date time string
76 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openrtc",
3 | "version": "1.0.0",
4 | "description": "An open source webrtc demo built with nextjs and tailwind",
5 | "main": "index.js",
6 | "private": true,
7 | "workspaces": [
8 | "packages/*"
9 | ],
10 | "engines": {
11 | "node": ">=14 <15"
12 | },
13 | "keywords": [],
14 | "author": "technikhil314",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "@commitlint/cli": "^12.0.1",
18 | "@commitlint/config-conventional": "^12.0.1",
19 | "babel-eslint": "^10.1.0",
20 | "eslint": "^7.21.0",
21 | "eslint-config-react-app": "^6.0.0",
22 | "eslint-plugin-flowtype": "^5.4.0",
23 | "eslint-plugin-import": "^2.22.1",
24 | "eslint-plugin-jsx-a11y": "^6.4.1",
25 | "eslint-plugin-react": "^7.22.0",
26 | "eslint-plugin-react-hooks": "^4.2.0",
27 | "husky": "^5.1.2",
28 | "lint-staged": "^10.5.4",
29 | "prettier": "^2.2.1"
30 | },
31 | "scripts": {
32 | "prepare": "husky install"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/backend/Procfile:
--------------------------------------------------------------------------------
1 | web: node --optimize_for_size --max_old_space_size=460 --gc_interval=100 ./index.js
2 |
--------------------------------------------------------------------------------
/packages/backend/https/domain.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDYDCCAkgCCQC5+N0+lvqB0zANBgkqhkiG9w0BAQsFADByMQswCQYDVQQGEwJJ
3 | TjELMAkGA1UECAwCTUgxCzAJBgNVBAcMAk9OMQswCQYDVQQKDAJOQTELMAkGA1UE
4 | CwwCTkExCzAJBgNVBAMMAk5BMSIwIAYJKoZIhvcNAQkBFhNkZW1vQG1haWxpbmF0
5 | b3IuY29tMB4XDTIxMDIwOTEzMzczMloXDTIyMDIwOTEzMzczMlowcjELMAkGA1UE
6 | BhMCSU4xCzAJBgNVBAgMAk1IMQswCQYDVQQHDAJPTjELMAkGA1UECgwCTkExCzAJ
7 | BgNVBAsMAk5BMQswCQYDVQQDDAJOQTEiMCAGCSqGSIb3DQEJARYTZGVtb0BtYWls
8 | aW5hdG9yLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANqRnudq
9 | KhaHEN9avRP/U0yhzCnqNXnofIvhwNIioy05flw4+8CLK8n3arM8+erUMZl1bnTq
10 | Xvz1Sn80hHzjlSXIt/1VwJ5jHojQlAjkOtqXrZV2469uZ2pF6rGhHc/aYdOvTZ/h
11 | KR0fLuVgmufYw4LG6IBlqFWdeeCTXal7nCuIbxF1UXMZeF9LOwcB/wk52YskTSns
12 | z0xAQ7lIF8uxx2aJ5t3957dtDhc2cN7m0NnYoWy6GIdJg/LAf5wckLChQv2EVh3D
13 | uJZUlj2zPqiNJQyVvISUGsHf7iaxB1jAMAa6wMXavCYxJACs+6FsYqkdGHGVIA1h
14 | Ne1tCAMAHeVBtwMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAHSQin9Y8XkYL6u/6
15 | UawurexYrkSdrPhn6AyPbC/oUV9pzt4XmwjRfFFOl8VQzFv/3VTE1y2TaXuwZPY9
16 | PyM1g5BPZ9+TzGOM9rydNUSqJ4UnLXVjtHEeb8lRINefVqpuztBBVVvikpafh02w
17 | 3/m968TqSG8UXJtT82VEeZ+6LjD22Q0UU+msrYqpKMU937s3iPxU16z9wXjqwe6o
18 | /oqbNJVJ6uFb9fPpnGkulUhuUNmNf9Ug+Zq+EzrkXwY8AWimLCft7xkkL+mM/PWt
19 | 4YZHbJe4sU7Va5DLdbdGtk0tl8jgwlhGN14x064AmwwsL8RX9x7tr3sA1Nd4CCJO
20 | MmftlA==
21 | -----END CERTIFICATE-----
22 |
--------------------------------------------------------------------------------
/packages/backend/https/domain.csr:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE REQUEST-----
2 | MIICtzCCAZ8CAQAwcjELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAk1IMQswCQYDVQQH
3 | DAJQTjELMAkGA1UECgwCTkExCzAJBgNVBAsMAk5BMQswCQYDVQQDDAJOQTEiMCAG
4 | CSqGSIb3DQEJARYTZGVtb0BtYWlsaW5hdG9yLmNvbTCCASIwDQYJKoZIhvcNAQEB
5 | BQADggEPADCCAQoCggEBAMNjpZ2fLycdWE06TwmAJ/e/58YEGX99Y66ODpLI91is
6 | o70ILcVYYre5YAGjvkn0Zn9F/uyuqQV69UTxdrsVWN23Y8JPOc+oqKBsk3azN8Ow
7 | wTkF125Iy4aN1W3gkXwpiSv+I1kP+UqZUGRdvyw2Y5RW7eC+Ygm2XFvQPUwD3i6I
8 | ZtiPy8aS3+QFLomu9zRTmkDGN4CXVgX4aepkSOryBk6SOBYbo2y89WH2V6WOpY0T
9 | TfjutPlO2+b8lKZ9K+sP16cWSJLbLaWOyu/HQWXrdh8OerrPsoBCNdDZAUwrDLI2
10 | ZdDArV4N8JAZdnlQk5xqPUFeKJtNtr/b1vtFJ9ybLmkCAwEAAaAAMA0GCSqGSIb3
11 | DQEBCwUAA4IBAQBUp34A+WFvIhxR/pFHzRoTk+eQIudQEXdtz0MdUKEX4h7fkSR9
12 | lWUvUnVhkZDC9pfG5HKQwrxEnfXYdTjImTU/4HvEeWATLcvKs0AJEdRlOPRxmj38
13 | FWcSz71KoMh4xajgKfHtfD6SbNPLXhtW+di4thlIK+oDxVfn7M7FzmBWWSnnjlHy
14 | R8be5VRbo4dvscJPoWInVEUVmj1qQBOaLHBydUhHVJoMmoOoezKJy9wJC0hjj5EX
15 | vCWBxdLFcvd0v91Y2Xe56r4zKgW37VSiubJRX5vaAhBJNn0Az1Xh/GI/BdTTZA5v
16 | AMQzFjTH40WfnkqxCbFsXceqK6K9HA9ngn+h
17 | -----END CERTIFICATE REQUEST-----
18 |
--------------------------------------------------------------------------------
/packages/backend/https/domain.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDakZ7naioWhxDf
3 | Wr0T/1NMocwp6jV56HyL4cDSIqMtOX5cOPvAiyvJ92qzPPnq1DGZdW506l789Up/
4 | NIR845UlyLf9VcCeYx6I0JQI5Dral62VduOvbmdqReqxoR3P2mHTr02f4SkdHy7l
5 | YJrn2MOCxuiAZahVnXngk12pe5wriG8RdVFzGXhfSzsHAf8JOdmLJE0p7M9MQEO5
6 | SBfLscdmiebd/ee3bQ4XNnDe5tDZ2KFsuhiHSYPywH+cHJCwoUL9hFYdw7iWVJY9
7 | sz6ojSUMlbyElBrB3+4msQdYwDAGusDF2rwmMSQArPuhbGKpHRhxlSANYTXtbQgD
8 | AB3lQbcDAgMBAAECggEAN4gmEm/Tz7NzYvw/6/PEK0U0hTW/boDWHAg4oGsygEwK
9 | Qc4skIgLkR3umymT6ckN7Qp8DPJ7PdPooJcsTjrYaygrLw95iARw/pvX8MZj+aPt
10 | 83qocKshVUv8TDM+StAWaN0yYLupYX/Jv3ejA+L8EZ8N9YlekpoXHCr08hm43Qot
11 | rqKLCO4GA4cTQAh4KQmkEUldXMfIJAsp8iloraIo4LRHEiAM8OfbgPbyAPjQrx4H
12 | a1QAre4+Uz4o2lhkQma3d/T3r1EAkhwrr80etBrei+PSeD9D6y678UmvOsD0CaoI
13 | rRsBDSpjDUZQHd4d0m6w7ycwNL4leD3DGfhfeeKScQKBgQD4TJfb59EAfqqaBhsA
14 | HRHckFbo6IuyH48p4hVHebLndk0u2JbTO2dtvB/9a5TK5uZkRSLEbzhA7VNGnx9D
15 | D/ZNOlfDJRvDdfo2xT0DS082ZC40rE104FTt6pA+xj6Sa8N1CR8TeGU03JT2E9Dc
16 | tTWbD1DpncpUAAzKDpSGkv/xqwKBgQDhWPqlGvjIieoHrCW0iHioBYbrp+y7BtnA
17 | S3hS+MPIauhU+QJMqdNYe6TVkk1+OXFI/MwrnTYcRSV0BLQkbWEGubZN0Aql2p5U
18 | xTbN9MdyW+0dHUPktaONT31/tL4OSmMsUzkcrTC12HoHHlq5ho2ixuPuq0SAwTeK
19 | YqHm9P6oCQKBgEzzt/DAVIbZutfHYqDTYZDA7x55y6mlnEH3vm6LagXQJTWKjJvk
20 | gjaBIkzxBYkorGiRAKhua7m7k56EfDTVgpkGpuJk4sjeDHDjCfi2Y1NREvziFZNO
21 | XyPpGVFLMWNBoK7p58ap/nu2jTgChi2Qv49R3Nq6O1VzOoN4p1FZx5bHAoGACGg2
22 | aaR49ZpXldOxUGvq/HHAV9ha95tI0mi+Y3IOc9KxOkJT+KI5VUq/mowrwfLIrC1q
23 | PJJP63wU6qAmTFmcThDtoTeKvidK0uTMp6BjNHwDe5uU5dp08Jevme0XThcuXf/4
24 | 2H4JnC8oVk2mmtdPP2xmIohXNOqAdPQ7EA/B1GkCgYBDC0rdfSIlN9oOfxWVoyKT
25 | bwN7CA/UzQJtwU1FxhsY8gK4kwdUM9nAL4gn2ensJGbKfkgP4pYxNr98bhNF49Vs
26 | Y9CLjpJ11GkrHhcpc8rm5o7KYcVPq5eqV7j8DCga2kWmasCcnOeQCkrAZYY8p9JM
27 | pTM4OzFfMbdTzSyHZpsYQQ==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/packages/backend/index.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const cryptoRandomString = require("crypto-random-string");
4 | let rooms = {};
5 | const fastifyOptions = {
6 | logger: true,
7 | };
8 | if (process.env.NODE_ENV !== "production") {
9 | fastifyOptions.https = {
10 | key: fs.readFileSync(path.join(__dirname, "https", "domain.key")),
11 | cert: fs.readFileSync(path.join(__dirname, "https", "domain.crt")),
12 | };
13 | }
14 | const fastify = require("fastify")(fastifyOptions);
15 | fastify.register(require("fastify-websocket"));
16 |
17 | fastify.get("/", { websocket: true }, (
18 | connection /* SocketStream */,
19 | req /* FastifyRequest */,
20 | reply
21 | ) => {
22 | let currentRoom = [];
23 | function handleConnect(serializedData) {
24 | const socket = connection.socket;
25 | const data = JSON.parse(serializedData);
26 | let currentRoomId = data.roomId,
27 | currentUserId;
28 | if (!currentRoomId) {
29 | currentRoomId = cryptoRandomString({ length: 10 });
30 | rooms[currentRoomId] = [];
31 | currentRoom = rooms[currentRoomId];
32 | } else if (!rooms[currentRoomId]) {
33 | rooms[currentRoomId] = [];
34 | } else {
35 | currentRoom = rooms[currentRoomId];
36 | }
37 | currentRoom = rooms[currentRoomId];
38 | currentRoom.push(socket);
39 | currentUserId = currentRoom.length - 1;
40 | socket.send(
41 | JSON.stringify({
42 | rtcContent: "connectSuccess",
43 | roomId: currentRoomId,
44 | userId: currentUserId,
45 | })
46 | );
47 | return currentUserId;
48 | }
49 |
50 | function broadcastNewConnection(userId, userName) {
51 | console.info("newPeer", userId);
52 | currentRoom.forEach((socket, index) => {
53 | index !== userId &&
54 | socket.send(
55 | JSON.stringify({
56 | rtcContent: "newPeer",
57 | by: userId,
58 | userName: userName,
59 | })
60 | );
61 | });
62 | }
63 |
64 | connection.socket.on("message", (serializedData) => {
65 | fastify.log.info(serializedData);
66 | const { type, ...rest } = JSON.parse(serializedData);
67 | if (type === "message") {
68 | console.info(
69 | `from ${rest.by} to ${rest.to} of type ${rest.rtcContent || type}`
70 | );
71 | }
72 | switch (type) {
73 | case "connect": {
74 | const userId = handleConnect(serializedData);
75 | broadcastNewConnection(userId, rest.userName);
76 | break;
77 | }
78 | case "message": {
79 | const destinationUser = currentRoom[rest.to];
80 | destinationUser.send(serializedData);
81 | break;
82 | }
83 | }
84 | });
85 | connection.socket.on("close", () => {
86 | const disconnectedUserId = currentRoom.indexOf(connection.socket);
87 | fastify.log.info({
88 | msg: `Client disconnected ${disconnectedUserId}`,
89 | });
90 | currentRoom.forEach((socket, index) => {
91 | index !== disconnectedUserId &&
92 | socket.send(
93 | JSON.stringify({
94 | rtcContent: "deletedPeer",
95 | peerId: disconnectedUserId,
96 | })
97 | );
98 | });
99 | });
100 | });
101 | // Run the server!
102 | fastify.listen(process.env.PORT || 4000, "0.0.0.0", function (err, address) {
103 | if (err) {
104 | fastify.log.error(err);
105 | process.exit(1);
106 | }
107 | fastify.log.info(`server listening on ${address}`);
108 | });
109 |
--------------------------------------------------------------------------------
/packages/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openrtc/backend",
3 | "version": "1.0.0",
4 | "description": "Backend for openrtc",
5 | "private": true,
6 | "main": "index.js",
7 | "scripts": {
8 | "start": "npx nodemon ./index.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "crypto-random-string": "^3.3.0",
15 | "fastify": "^3.11.0",
16 | "fastify-websocket": "^3.0.0",
17 | "nodemon": "^2.0.7",
18 | "socket-io": "^1.0.0"
19 | }
20 | }
--------------------------------------------------------------------------------
/packages/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 |
--------------------------------------------------------------------------------
/packages/frontend/components/errorModal.jsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import Modal from "../components/modal";
3 | export default function ErrorModal(params) {
4 | const router = useRouter();
5 | const onModalClose = () => {
6 | router.push("/meeting");
7 | };
8 | return (
9 |
10 |
11 | Opps.... Your browser does not support required features to record video. We recommend using latest
12 | version of chrome dekstop.
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/frontend/components/externalLink.jsx:
--------------------------------------------------------------------------------
1 | export default function ExternalLink({ href, className, children }) {
2 | return (
3 |
9 | {children}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/packages/frontend/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import ExternalLink from "./externalLink";
2 |
3 | export default function Footer() {
4 | return (
5 |
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/packages/frontend/components/localVideos.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { useRhinoState } from "../store/states";
3 | import { userMediaConstraints } from "../utils/constants";
4 | export const LocalVideo = ({ isVlog }) => {
5 | const localVideoElement = useRef();
6 | const [localStream, setLocalStream] = useRhinoState("localStream");
7 | const [shareScreen, setShareScreen] = useRhinoState("shareScreen");
8 | const [isStarted] = useRhinoState("isStarted");
9 |
10 | useEffect(() => {
11 | let stream = {},
12 | normalLocalStream;
13 | (async () => {
14 | let videoTrack, audioTrack;
15 | let normalLocalStream = await navigator.mediaDevices.getUserMedia(userMediaConstraints);
16 | if (shareScreen) {
17 | stream = await navigator.mediaDevices.getDisplayMedia({
18 | video: true,
19 | });
20 | stream.oninactive = () => {
21 | setShareScreen(false);
22 | };
23 | videoTrack = stream.getVideoTracks()[0];
24 | } else {
25 | videoTrack = normalLocalStream.getVideoTracks()[0];
26 | }
27 | audioTrack = normalLocalStream.getAudioTracks()[0];
28 | setLocalStream(new MediaStream([audioTrack, videoTrack]));
29 | })();
30 | return () => {
31 | normalLocalStream && normalLocalStream.getTracks().forEach((x) => x.stop());
32 | };
33 | }, [shareScreen, isStarted, isVlog]);
34 | useEffect(() => {
35 | if (isStarted) {
36 | localVideoElement.current.srcObject = localStream;
37 | localVideoElement.current.play();
38 | }
39 | return async () => {
40 | localStream && localStream.getTracks().forEach((x) => x.stop());
41 | };
42 | }, [localStream]);
43 | return isStarted ? (
44 | {
50 | var style = getComputedStyle(event.target, null);
51 | event.dataTransfer.setData(
52 | "text/plain",
53 | `localVideo,${parseInt(style.getPropertyValue("left"), 10) - event.clientX},${
54 | parseInt(style.getPropertyValue("top"), 10) - event.clientY
55 | }`
56 | );
57 | }}
58 | >
59 |
68 |
69 | ) : null;
70 | };
71 |
72 | LocalVideo.displayName = "LocalVideo";
73 |
--------------------------------------------------------------------------------
/packages/frontend/components/modal.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export default function Modal({ title, children, onClose = () => {} }) {
4 | const [isOpen, setIsOpen] = useState(true);
5 | const dialogProp = isOpen
6 | ? {
7 | open: isOpen,
8 | }
9 | : {};
10 | const closeModal = () => {
11 | setIsOpen(false);
12 | onClose();
13 | };
14 | return (
15 |
16 |
17 |
21 |
25 | {title}
26 |
27 |
28 | ×
29 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/packages/frontend/components/navbar.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { useState } from "react";
3 | import { classNames } from "../utils/classNames";
4 | import { text } from "../utils/constants";
5 | import ExternalLink from "./externalLink";
6 | import { useRouter } from "next/router";
7 | export default function Navbar() {
8 | const [isOpen, setIsOpen] = useState(false);
9 | const router = useRouter();
10 | return (
11 | <>
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
setIsOpen(!isOpen)}
31 | >
32 | Open main menu
33 |
44 |
45 |
46 |
57 |
58 |
59 |
60 |
68 |
106 |
107 |
108 |
109 | >
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/packages/frontend/components/remoteStreams.jsx:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useState } from "react";
2 | import { useRhinoState } from "../store/states";
3 | import { iceConfig } from "../utils/constants";
4 | import RemoteVideos from "./remoteVideos";
5 | export const RemoteStreams = ({ myUserId, socket }) => {
6 | const [peers, setPeers] = useState({});
7 | const [userNames, setUserNames] = useState([]);
8 | const [localStream] = useRhinoState("localStream");
9 | const [userName] = useRhinoState("userName");
10 | const getPeerConnection = (peerId) => {
11 | const peer = peers[peerId];
12 | let sendChannel, receiveChannel;
13 | if (!peer) {
14 | const newPeerConnection = new RTCPeerConnection(iceConfig);
15 | const channelCallback = (event) => {
16 | receiveChannel = event.channel;
17 | receiveChannel.onmessage = (event) => {
18 | setUserNames((userNames) => {
19 | userNames[peerId] = JSON.parse(event.data).userName;
20 | return [...userNames];
21 | });
22 | };
23 | };
24 | sendChannel = newPeerConnection.createDataChannel("sendChannel");
25 | sendChannel.onopen = () => {
26 | sendChannel.send(
27 | JSON.stringify({
28 | userName,
29 | })
30 | );
31 | };
32 | newPeerConnection.ondatachannel = channelCallback;
33 | let camVideoTrack = localStream.getVideoTracks()[0];
34 | let camAudioTrack = localStream.getAudioTracks()[0];
35 | newPeerConnection.addTrack(camVideoTrack, localStream);
36 | newPeerConnection.addTrack(camAudioTrack, localStream);
37 | const setNewPeers = (stream) => {
38 | setPeers((peers) => {
39 | if (!peers[peerId] || stream) {
40 | peers[peerId] = {
41 | connection: newPeerConnection,
42 | sendChannel: sendChannel,
43 | receiveChannel: receiveChannel,
44 | stream,
45 | };
46 | return {
47 | ...peers,
48 | };
49 | }
50 | return peers;
51 | });
52 | };
53 | newPeerConnection.onicecandidate = (e) => {
54 | socket.send(
55 | JSON.stringify({
56 | by: myUserId,
57 | to: peerId,
58 | ice: e.candidate,
59 | type: "message",
60 | rtcContent: "ice",
61 | })
62 | );
63 | setNewPeers();
64 | };
65 | newPeerConnection.onaddstream = (event) => {
66 | setNewPeers(event.stream);
67 | };
68 | newPeerConnection.onremovestream = (event) => {
69 | setPeers((peers) => {
70 | peers[peerId].stream = null;
71 | return {
72 | ...peers,
73 | };
74 | });
75 | };
76 | setNewPeers();
77 | peers[peerId] = {
78 | connection: newPeerConnection,
79 | sendChannel: sendChannel,
80 | receiveChannel: receiveChannel,
81 | };
82 | }
83 | return peers[peerId];
84 | };
85 | const makeOffer = ({ by }) => {
86 | if (by === myUserId) {
87 | return;
88 | }
89 | let currentPeerConnection = getPeerConnection(by).connection;
90 | currentPeerConnection.createOffer(
91 | (sdp) => {
92 | currentPeerConnection.setLocalDescription(sdp);
93 | socket.send(
94 | JSON.stringify({
95 | by: myUserId,
96 | to: by,
97 | sdp: sdp,
98 | type: "message",
99 | rtcContent: "peerOffer",
100 | })
101 | );
102 | },
103 | (error) => {
104 | console.error(error, "createOffer");
105 | },
106 | { mandatory: { offerToReceiveVideo: true, offerToReceiveAudio: true } }
107 | );
108 | };
109 | const setRemoteDescription = ({ by, sdp }) => {
110 | return new Promise((resolve, reject) => {
111 | const currentPeerConnection = getPeerConnection(by).connection;
112 | currentPeerConnection.setRemoteDescription(
113 | new RTCSessionDescription(sdp),
114 | () => {
115 | resolve();
116 | },
117 | (error) => {
118 | console.error(error, "setRemoteDescription");
119 | reject();
120 | }
121 | );
122 | });
123 | };
124 | const sendAnswer = ({ by }) => {
125 | const currentPeerConnection = getPeerConnection(by).connection;
126 | currentPeerConnection.createAnswer(
127 | (sdp) => {
128 | currentPeerConnection.setLocalDescription(sdp);
129 | socket.send(
130 | JSON.stringify({
131 | by: myUserId,
132 | to: by,
133 | sdp: sdp,
134 | type: "message",
135 | rtcContent: "peerAnswer",
136 | })
137 | );
138 | },
139 | (e) => {
140 | console.error(e, "setLocalDescription");
141 | }
142 | );
143 | };
144 | const addIceCandidate = ({ by, ice }) => {
145 | const currentPeerConnection = getPeerConnection(by).connection;
146 | ice && currentPeerConnection.addIceCandidate(new RTCIceCandidate(ice));
147 | };
148 | const deletePeer = ({ peerId }) => {
149 | setPeers((peers) => {
150 | delete peers[peerId];
151 | return {
152 | ...peers,
153 | };
154 | });
155 | };
156 | const addSocketMessageHandlers = () => {
157 | socket.onmessage = async function (event) {
158 | const { rtcContent, type, ...rest } = JSON.parse(event.data);
159 | switch (rtcContent) {
160 | case "newPeer":
161 | makeOffer(rest);
162 | break;
163 | case "peerOffer":
164 | await setRemoteDescription(rest);
165 | sendAnswer(rest);
166 | break;
167 | case "peerAnswer":
168 | await setRemoteDescription(rest);
169 | break;
170 | case "ice":
171 | addIceCandidate(rest);
172 | break;
173 | case "deletedPeer":
174 | deletePeer(rest);
175 | break;
176 | }
177 | };
178 | };
179 | useLayoutEffect(() => {
180 | addSocketMessageHandlers();
181 | }, []);
182 | useLayoutEffect(() => {
183 | for (let prop in peers) {
184 | const peerConnection = peers[prop].connection;
185 | let videoSender = peerConnection
186 | .getSenders()
187 | .find((x) => x.track.kind === "video");
188 | if (localStream) {
189 | let newVideoTrack = localStream.getVideoTracks()[0];
190 | videoSender.replaceTrack(newVideoTrack);
191 | }
192 | }
193 | }, [localStream]);
194 | return (
195 |
198 | );
199 | };
200 |
--------------------------------------------------------------------------------
/packages/frontend/components/remoteVideos.jsx:
--------------------------------------------------------------------------------
1 | import Video from "./video";
2 |
3 | export default function RemoteVideos({ peers, userNames }) {
4 | const remoteVideos = [];
5 | for (let peerId in peers) {
6 | let peer = peers[peerId];
7 | remoteVideos.push(
8 |
13 | );
14 | }
15 | return remoteVideos;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/frontend/components/roomDetails.jsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { useEffect, useRef } from "react";
3 | import { useRhinoState } from "../store/states";
4 |
5 | export default function RoomDetails() {
6 | const [, setRoomName] = useRhinoState("roomName");
7 | const router = useRouter();
8 | const input = useRef();
9 | const handleSubmit = (e) => {
10 | e.preventDefault();
11 | const formData = new FormData(e.target);
12 | const roomName = formData.get("roomName").toLowerCase().replace(/\s+/g, "-").trim();
13 | router.push(`/${roomName}`);
14 | setRoomName(roomName);
15 | };
16 | useEffect(() => {
17 | input.current.focus();
18 | });
19 | return (
20 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/packages/frontend/components/userDetails.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { useRhinoState } from "../store/states";
3 | import { useRouter } from "next/router";
4 |
5 | export default function UserDetails() {
6 | const [, setIsStarted] = useRhinoState("isStarted");
7 | const [, setUserName] = useRhinoState("userName");
8 | const [, setRoomName] = useRhinoState("roomName");
9 | const router = useRouter();
10 | const input = useRef();
11 | useEffect(() => {
12 | input.current.focus();
13 | setRoomName(router.query.roomName);
14 | });
15 | const handleSubmit = (e) => {
16 | e.preventDefault();
17 | const formData = new FormData(e.target);
18 | const userName = formData.get("userName");
19 | setUserName(userName);
20 | setIsStarted(true);
21 | };
22 | return (
23 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/packages/frontend/components/video.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | export default function Video({ peer, userName }) {
4 | const vRef = useRef();
5 | useEffect(() => {
6 | if (vRef.current) {
7 | vRef.current.srcObject = peer.stream;
8 | vRef.current.play();
9 | }
10 | }, [peer]);
11 | return peer && peer.stream ? (
12 |
16 |
22 |
23 | ) : null;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/frontend/components/vlogVideo.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable-next-line */
2 | import * as tf from "@tensorflow/tfjs";
3 | import * as bodyPix from "@tensorflow-models/body-pix";
4 | import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
5 | import { classNames } from "../utils/classNames";
6 | import { loadBodyPix, readAsObjectURL, rgb2hsl } from "../utils/helpers";
7 | let backgroundBlurAmount = 6;
8 | let edgeBlurAmount = 0;
9 | let enableMirrorEffect = false;
10 | let enableVirtualBackground = false;
11 | let removeBackground = false;
12 | let enableBlur = false;
13 | let backgroundImage = null;
14 | let backgroundColor = "#FFFFFF";
15 | let enableGreenScreen = false;
16 | function VlogVideo({ isRecording, config }, ref) {
17 | const localVideoElement = useRef();
18 | const canvasRef = useRef();
19 | const displayVideoElement = useRef();
20 | const [customBackgroundImage, setCustomBackgroundImage] = useState();
21 | const isWithoutVideo = config.withoutVideo === "on";
22 | let displayStream = useRef(),
23 | normalLocalStream = useRef();
24 |
25 | const cleanUp = () => {
26 | displayStream.current && displayStream.current.getTracks().forEach((x) => x.stop());
27 | normalLocalStream.current && normalLocalStream.current.getTracks().forEach((x) => x.stop());
28 | displayStream.current = normalLocalStream.current = null;
29 | };
30 |
31 | const blurVideoBg = (segmentation) => {
32 | let _backgroundBlurAmount = enableBlur ? backgroundBlurAmount : 0;
33 | bodyPix.drawBokehEffect(
34 | canvasRef.current,
35 | localVideoElement.current,
36 | segmentation,
37 | _backgroundBlurAmount,
38 | edgeBlurAmount
39 | );
40 | };
41 |
42 | async function removeBg(segmentation) {
43 | const foregroundColor = { r: 0, g: 0, b: 0, a: 255 };
44 | const backgroundColor = { r: 0, g: 0, b: 0, a: 0 };
45 | const backgroundDarkeningMask = bodyPix.toMask(segmentation, foregroundColor, backgroundColor);
46 | const ctx = canvasRef.current.getContext("2d");
47 | ctx.putImageData(backgroundDarkeningMask, 0, 0);
48 | ctx.globalCompositeOperation = "source-in";
49 | ctx.drawImage(localVideoElement.current, 0, 0, canvasRef.current.width, canvasRef.current.height);
50 | }
51 | function addVirtualBg() {
52 | const ctx = canvasRef.current.getContext("2d");
53 | ctx.globalCompositeOperation = "destination-atop";
54 | ctx.drawImage(backgroundImage, 0, 0, canvasRef.current.width, canvasRef.current.height);
55 | }
56 |
57 | function addSolidBg() {
58 | const ctx = canvasRef.current.getContext("2d");
59 | ctx.globalCompositeOperation = "destination-atop";
60 | ctx.beginPath();
61 | ctx.rect(0, 0, canvasRef.current.width, canvasRef.current.height);
62 | ctx.fillStyle = backgroundColor;
63 | ctx.fill();
64 | }
65 |
66 | const replaceGreenScreen = (e) => {
67 | const ctx = canvasRef.current.getContext("2d");
68 | let frame = ctx.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height);
69 | let data = frame.data;
70 | let len = data.length;
71 | for (let j = 0; j < len; j += 4) {
72 | let hsl = rgb2hsl(data[j], data[j + 1], data[j + 2]);
73 | let h = hsl[0],
74 | s = hsl[1],
75 | l = hsl[2];
76 | if (h >= 90 && h <= 160 && s >= 25 && s <= 90 && l >= 20 && l <= 75) {
77 | data[j + 3] = 0;
78 | }
79 | }
80 | ctx.putImageData(frame, 0, 0);
81 | };
82 |
83 | const processVideo = async (net) => {
84 | const segmentation = await net.segmentPerson(localVideoElement.current, {
85 | flipHorizontal: false,
86 | internalResolution: "medium",
87 | segmentationThreshold: 0.5,
88 | });
89 | if (enableVirtualBackground) {
90 | removeBg(segmentation);
91 | addVirtualBg();
92 | } else if (removeBackground) {
93 | removeBg(segmentation);
94 | addSolidBg();
95 | } else if (enableBlur) {
96 | blurVideoBg(segmentation);
97 | } else {
98 | const ctx = canvasRef.current.getContext("2d");
99 | ctx.globalCompositeOperation = "source-over";
100 | ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
101 | ctx.drawImage(localVideoElement.current, 0, 0, canvasRef.current.width, canvasRef.current.height);
102 | }
103 | if (enableGreenScreen) {
104 | replaceGreenScreen();
105 | }
106 | requestAnimationFrame(() => {
107 | processVideo(net);
108 | });
109 | };
110 | useImperativeHandle(
111 | ref,
112 | () => ({
113 | getStream: () => {
114 | return new MediaStream([
115 | displayStream.current.getVideoTracks()[0],
116 | normalLocalStream.current.getAudioTracks()[0],
117 | ]);
118 | },
119 | }),
120 | [displayStream.current, normalLocalStream.current]
121 | );
122 |
123 | useEffect(() => {
124 | let objectUrl;
125 | if (customBackgroundImage) {
126 | (async () => {
127 | objectUrl = await readAsObjectURL(customBackgroundImage);
128 | const img = new Image();
129 | img.src = objectUrl;
130 | img.onload = () => {
131 | backgroundImage = img;
132 | };
133 | })();
134 | }
135 | return () => {
136 | objectUrl && URL.revokeObjectURL(objectUrl);
137 | };
138 | }, [customBackgroundImage]);
139 | useEffect(() => {
140 | const img = new Image();
141 | img.src = "/virtual-bgs/1.jpg";
142 | img.onload = () => {
143 | backgroundImage = img;
144 | };
145 | }, []);
146 | useEffect(() => {
147 | if (isRecording && !isWithoutVideo) {
148 | displayVideoElement.current.srcObject = canvasRef.current.captureStream(60);
149 | displayVideoElement.current.play();
150 | if (document.pictureInPictureEnabled) {
151 | displayVideoElement.current.addEventListener("loadedmetadata", () => {
152 | displayVideoElement.current.requestPictureInPicture();
153 | });
154 | }
155 | }
156 | return async () => {
157 | if (document.pictureInPictureElement) {
158 | await document.exitPictureInPicture();
159 | }
160 | };
161 | }, [isRecording]);
162 | useEffect(() => {
163 | (async () => {
164 | try {
165 | displayStream.current = await navigator.mediaDevices.getDisplayMedia({
166 | video: {},
167 | });
168 | let finalMediaConstraints = {
169 | audio: {},
170 | video: {},
171 | };
172 | if (isWithoutVideo) {
173 | delete finalMediaConstraints.video;
174 | }
175 | normalLocalStream.current = await navigator.mediaDevices.getUserMedia(finalMediaConstraints);
176 | if (!isWithoutVideo) {
177 | localVideoElement.current.srcObject = normalLocalStream.current;
178 | localVideoElement.current.play();
179 | localVideoElement.current.addEventListener("loadeddata", async () => {
180 | const net = await loadBodyPix();
181 | processVideo(net);
182 | });
183 | }
184 | } catch (err) {
185 | cleanUp();
186 | }
187 | })();
188 | return cleanUp;
189 | }, []);
190 | if (isWithoutVideo) {
191 | return null;
192 | }
193 | return (
194 |
195 |
196 |
197 |
198 | Enable green screen
199 |
200 | (enableGreenScreen = !enableGreenScreen)}
206 | />
207 |
208 |
209 |
210 |
211 |
212 | Enable virtual background
213 |
214 | (enableVirtualBackground = !enableVirtualBackground)}
220 | />
221 |
222 |
223 |
224 | Custom Image
225 |
226 |
227 | setCustomBackgroundImage(e.target.files[0])}
233 | />
234 |
235 | Choose file
236 |
237 |
238 |
239 |
240 |
270 |
298 |
299 |
300 | This is how you will appear in video at bottom right corner but once recording starts you can drag and
301 | drop yourself anywhere in the screen and resize yourself too
302 |
303 |
312 |
324 |
337 |
338 |
339 | );
340 | }
341 |
342 | export default forwardRef(VlogVideo);
343 |
--------------------------------------------------------------------------------
/packages/frontend/hooks/pageVisibility.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function usePageVisibility() {
4 | const [isVisible, setIsVisible] = useState(true);
5 | useEffect(() => {
6 | let hidden, visibilityChange;
7 | if (typeof document.hidden !== "undefined") {
8 | // Opera 12.10 and Firefox 18 and later support
9 | hidden = "hidden";
10 | visibilityChange = "visibilitychange";
11 | } else if (typeof document.msHidden !== "undefined") {
12 | hidden = "msHidden";
13 | visibilityChange = "msvisibilitychange";
14 | } else if (typeof document.webkitHidden !== "undefined") {
15 | hidden = "webkitHidden";
16 | visibilityChange = "webkitvisibilitychange";
17 | }
18 | const handleVisibilityChange = (e) => {
19 | e.stopPropagation();
20 | if (document[hidden]) {
21 | setIsVisible(false);
22 | } else {
23 | setIsVisible(true);
24 | }
25 | };
26 | window.onblur = (e) => {
27 | setIsVisible(false);
28 | };
29 | window.onfocus = (e) => {
30 | setIsVisible(true);
31 | };
32 | document.addEventListener(visibilityChange, handleVisibilityChange, true);
33 | return () => {
34 | document.removeEventListener(visibilityChange, handleVisibilityChange);
35 | };
36 | }, []);
37 | return isVisible;
38 | }
39 |
--------------------------------------------------------------------------------
/packages/frontend/hooks/socketConnection.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { useEffect, useState } from "react";
3 |
4 | export default function useSocketConnetion(isStarted, userName) {
5 | const [state, setState] = useState({});
6 | const router = useRouter();
7 | useEffect(() => {
8 | if (isStarted) {
9 | const socket = new WebSocket(process.env.NEXT_PUBLIC_WEBSOCKET_URL);
10 | socket.onopen = function () {
11 | const initialData = { type: "connect" };
12 | if (router.query && router.query.roomName) {
13 | initialData.roomId = router.query.roomName;
14 | initialData.userName = userName;
15 | }
16 | socket.send(JSON.stringify(initialData));
17 | };
18 | socket.onmessage = async function (event) {
19 | const { rtcContent, type, ...rest } = JSON.parse(event.data);
20 | switch (rtcContent) {
21 | case "connectSuccess": {
22 | const { roomId, userId } = rest;
23 | setState({
24 | roomId,
25 | userId,
26 | socket,
27 | });
28 | break;
29 | }
30 | }
31 | };
32 | }
33 | }, [isStarted]);
34 | return state;
35 | }
36 |
--------------------------------------------------------------------------------
/packages/frontend/hooks/useGetDevices.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { debug } from "../utils/helpers";
3 |
4 | export default function useGetDevices() {
5 | let defaultDevices = {
6 | audio: [],
7 | video: [],
8 | };
9 | const [devices, setDevices] = useState(defaultDevices);
10 | debug(devices);
11 | useEffect(() => {
12 | (async () => {
13 | if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
14 | return;
15 | }
16 | try {
17 | const cameraPermission = await navigator.permissions.query({ name: "camera" });
18 | const micPermission = await navigator.permissions.query({ name: "microphone" });
19 | debug({ cameraPermission, micPermission });
20 | if (cameraPermission.state !== "granted" || micPermission.state !== "granted") {
21 | const stream = await navigator.mediaDevices.getUserMedia({
22 | video: true,
23 | audio: true,
24 | });
25 | if (!stream) {
26 | return;
27 | }
28 | stream.getVideoTracks().forEach((x) => x.stop());
29 | stream.getAudioTracks().forEach((x) => x.stop());
30 | }
31 | const devices = await navigator.mediaDevices.enumerateDevices();
32 | //toggleLoader()
33 | defaultDevices = devices.reduce((acc, device) => {
34 | if (device.kind === "videoinput") {
35 | acc.video.push(device);
36 | }
37 | if (device.kind === "audioinput") {
38 | acc.audio.push(device);
39 | }
40 | return acc;
41 | }, defaultDevices);
42 | setDevices({ ...defaultDevices });
43 | } catch (err) {
44 | console.error(err);
45 | }
46 | })();
47 | }, []);
48 | return devices;
49 | }
50 |
--------------------------------------------------------------------------------
/packages/frontend/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | target: "serverless",
3 | async redirects() {
4 | return [
5 | {
6 | source: "/vlog",
7 | destination: "/",
8 | permanent: true,
9 | },
10 | ];
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/packages/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openrtc/frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "Frontend for openrtc",
6 | "scripts": {
7 | "serve": "next start",
8 | "build": "next build",
9 | "start": "npx next dev",
10 | "export": "next export"
11 | },
12 | "dependencies": {
13 | "modern-normalize": "^1.0.0",
14 | "next": "10.0.6",
15 | "nextjs-sitemap-generator": "^1.3.1",
16 | "node-sass": "^5.0.0",
17 | "react": "17.0.1",
18 | "react-dom": "17.0.1",
19 | "react-rhino": "^1.3.0",
20 | "sharp": "^0.28.0",
21 | "vercel": "^21.3.3",
22 | "webrtc-adapter": "^7.7.0",
23 | "@tensorflow-models/body-pix": "^2.2.0",
24 | "@tensorflow/tfjs": "^3.9.0",
25 | "comlink": "^4.3.1"
26 | },
27 | "devDependencies": {
28 | "autoprefixer": "^10.2.4",
29 | "postcss": "^8.2.6",
30 | "tailwindcss": "2.1"
31 | }
32 | }
--------------------------------------------------------------------------------
/packages/frontend/pages/[roomName].jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { useRouter } from "next/router";
3 | import { LocalVideo } from "../components/localVideos";
4 | import { RemoteStreams } from "../components/remoteStreams";
5 | import UserDetails from "../components/userDetails";
6 | import { classNames } from "../utils/classNames";
7 | import useSocketConnection from "../hooks/socketConnection";
8 | import { useRhinoState } from "../store/states";
9 | import { text } from "../utils/constants";
10 | import { capitalize } from "../utils/helpers";
11 |
12 | export default function Main() {
13 | const [isStarted, setIsStarted] = useRhinoState("isStarted");
14 | const [userName] = useRhinoState("userName");
15 | const [localStream] = useRhinoState("localStream");
16 | const [shareScreen, setShareScreen] = useRhinoState("shareScreen");
17 | const [roomName] = useRhinoState("roomName");
18 | const { userId: myUserId, socket } = useSocketConnection(isStarted, userName);
19 | const router = useRouter();
20 | const stop = () => {
21 | router.push("/");
22 | setIsStarted(false);
23 | };
24 | const copyToClipboard = () => {
25 | const el = document.createElement("textarea");
26 | el.value = `${process.env.NEXT_PUBLIC_URL}/${roomName}`;
27 | el.classList.add("opacity-0", "h-1", "w-1", "absolute", "top-0", "-z-1");
28 | document.body.appendChild(el);
29 | el.select();
30 | document.execCommand("copy");
31 | document.body.removeChild(el);
32 | };
33 | const PageHead = () => (
34 |
35 | {`${capitalize(roomName || "Your room")} | ${text.appName}`}
36 |
37 | );
38 | if (!isStarted) {
39 | return (
40 | <>
41 |
42 |
43 | An open source alternative to video conferencing
44 |
45 |
46 |
47 |
51 | Copy invite link
52 |
53 |
54 |
55 |
56 | Hello, This is an attempt to respect your privacy.
57 |
58 |
59 |
60 |
61 | >
62 | );
63 | }
64 | return (
65 | <>
66 |
67 |
68 |
73 | Stop
74 |
75 | setShareScreen(!shareScreen)}
83 | >
84 | {shareScreen ? "Stop share" : "Share screen"}
85 |
86 |
87 |
88 | {socket && localStream && }
89 | >
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/packages/frontend/pages/_app.jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Footer from "../components/footer";
3 | import Navbar from "../components/navbar";
4 | import { RhinoProvider } from "../store/states";
5 | import "../styles/globals.scss";
6 | import { text } from "../utils/constants";
7 | function MyApp({ Component, pageProps }) {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
15 |
24 | {/* For discord */}
25 |
31 | {/* For browser */}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {/* For telegram */}
51 |
57 |
63 | {/* for facebook */}
64 |
70 | {/* for whatsapp */}
71 |
77 | {/* for linkedin 800x800 ideal is 1200x695 */}
78 |
84 |
85 |
86 | {/* for twitter */}
87 |
88 |
89 | `,
97 | }}
98 | >
99 |
100 |
101 | {
104 | var data = event.dataTransfer.getData("text/plain").split(",");
105 | let dm = document.getElementById(data[0]);
106 | dm.style.left = event.clientX + parseInt(data[1], 10) + "px";
107 | dm.style.top = event.clientY + parseInt(data[2], 10) + "px";
108 | }}
109 | onDragOver={(event) => {
110 | event.preventDefault();
111 | return false;
112 | }}
113 | >
114 |
115 |
116 |
117 |
118 | >
119 | );
120 | }
121 |
122 | export default MyApp;
123 |
--------------------------------------------------------------------------------
/packages/frontend/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default (req, res) => {
4 | res.status(200).json({ name: 'John Doe' })
5 | }
6 |
--------------------------------------------------------------------------------
/packages/frontend/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { useEffect, useRef, useState } from "react";
3 | import ErrorModal from "../components/errorModal";
4 | import VlogVideo from "../components/vlogVideo";
5 | import usePageVisibility from "../hooks/pageVisibility";
6 | import { classNames } from "../utils/classNames";
7 | import { text } from "../utils/constants";
8 | import { capitalize, getBrowserName } from "../utils/helpers";
9 | import useGetDevices from "../hooks/useGetDevices";
10 | export async function getStaticProps() {
11 | return {
12 | props: {},
13 | };
14 | }
15 | export default function Vlog() {
16 | const [isRecording, setIsRecording] = useState(false);
17 | const [initialState, setInitialState] = useState(false);
18 | const [showError, setShowError] = useState();
19 | const [mediaRecorder, setMediaRecorder] = useState();
20 | const [recorderState, setRecorderState] = useState();
21 | const isPageVisible = usePageVisibility();
22 | const devices = useGetDevices();
23 | const streamRef = useRef();
24 | const defaultAudioDevice = devices.audio.find((x) => x.deviceId === "default") || {};
25 | const defaultVideoDevice = devices.video.find((x) => x.deviceId === "default") || {};
26 | const isWithoutVideo = initialState.withoutVideo === "on";
27 | const resetState = () => {
28 | setIsRecording(false);
29 | setMediaRecorder(null);
30 | setRecorderState("");
31 | setInitialState(false);
32 | };
33 |
34 | const handleFormSubmit = (e) => {
35 | e.preventDefault();
36 | const formData = new FormData(e.target);
37 | let json = {
38 | audioDevice: defaultAudioDevice.deviceId,
39 | videoDevice: defaultVideoDevice.deviceId,
40 | };
41 | formData.forEach((value, key) => {
42 | json[key] = value;
43 | });
44 | setInitialState(json);
45 | };
46 |
47 | const handleRecording = async () => {
48 | if (initialState && !isRecording) {
49 | const mediaRec = new MediaRecorder(streamRef.current.getStream());
50 | setMediaRecorder(mediaRec);
51 | setIsRecording(true);
52 | } else if (isRecording && initialState) {
53 | if (mediaRecorder.state !== "inactive") {
54 | mediaRecorder.stop();
55 | } else {
56 | resetState();
57 | }
58 | }
59 | };
60 | useEffect(() => {
61 | const browserName = getBrowserName(navigator.userAgent);
62 | if (!document.pictureInPictureEnabled || (browserName !== "chrome" && browserName !== "firefox")) {
63 | setShowError(true);
64 | }
65 | }, []);
66 | useEffect(() => {
67 | if (mediaRecorder) {
68 | let recordedChunks = [];
69 | mediaRecorder.addEventListener("dataavailable", (event) => {
70 | if (event.data.size > 0) {
71 | recordedChunks.push(event.data);
72 | }
73 | });
74 | mediaRecorder.addEventListener("stop", () => {
75 | let blob = new Blob(recordedChunks, {
76 | type: "video/webm",
77 | });
78 | let url = URL.createObjectURL(blob);
79 | let a = document.createElement("a");
80 | document.body.appendChild(a);
81 | a.style = "display: none";
82 | a.href = url;
83 | a.download = `${new Date().toISOString()}.webm`;
84 | a.click();
85 | window.URL.revokeObjectURL(url);
86 | resetState();
87 | });
88 | setRecorderState(capitalize(mediaRecorder.state));
89 | }
90 | }, [mediaRecorder]);
91 | useEffect(() => {
92 | if (mediaRecorder) {
93 | if (!isPageVisible && mediaRecorder.state === "inactive") {
94 | mediaRecorder.start();
95 | } else if (mediaRecorder.state === "recording") {
96 | mediaRecorder.pause && mediaRecorder.pause();
97 | } else if (mediaRecorder.state === "paused") {
98 | mediaRecorder.resume && mediaRecorder.resume();
99 | }
100 | setRecorderState(capitalize(mediaRecorder.state));
101 | }
102 | }, [isPageVisible]);
103 |
104 | const pageTitle = mediaRecorder && recorderState;
105 | return (
106 | <>
107 |
108 | {`${pageTitle || "Vlog"} | ${text.appName}`}
109 |
110 |
111 | {showError && }
112 |
113 | Simple in browser video recording for developers to create live coding videos.
114 |
115 |
122 |
123 |
What all can you do here?
124 |
130 |
131 | You can record your screen along with you in the video and store the recording To record click
132 | button below.
133 |
134 |
135 |
136 |
219 | {initialState && (
220 | <>
221 |
Read this before clicking on the button below
222 |
228 | This works all on your device locally. No data is sent to any server.
229 | The recording will automatically pause when you focus on this window.
230 | The title of this browser window will tell you the status.
231 | The recording will start/resume when you focus on other windows.
232 | Click on the button below anytime to stop and download the recording.
233 |
234 | >
235 | )}
236 |
248 | {`${pageTitle || "Start recording"}...`}
249 |
250 |
251 | {initialState && (
252 |
253 |
254 |
255 | )}
256 |
257 | >
258 | );
259 | }
260 |
--------------------------------------------------------------------------------
/packages/frontend/pages/meeting.jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import RoomDetails from "../components/roomDetails";
3 | import { text } from "../utils/constants";
4 | const pageTitle = `${text.appName} | ${text.titleDesc}`;
5 | export async function getStaticProps() {
6 | return {
7 | props: {},
8 | };
9 | }
10 | export default function Main() {
11 | return (
12 | <>
13 |
14 | {pageTitle}
15 |
16 |
17 |
18 |
19 | An open source alternative to video conferencing
20 |
21 |
22 |
23 |
What all can you do here?
24 |
25 | You can create conference room and invite others to join
26 |
27 |
28 |
29 |
30 |
31 |
32 | Hello, This is an attempt to respect your privacy.
33 |
34 |
35 |
36 |
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/packages/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/packages/frontend/public/114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/114x114.png
--------------------------------------------------------------------------------
/packages/frontend/public/120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/120x120.png
--------------------------------------------------------------------------------
/packages/frontend/public/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/128x128.png
--------------------------------------------------------------------------------
/packages/frontend/public/144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/144x144.png
--------------------------------------------------------------------------------
/packages/frontend/public/152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/152x152.png
--------------------------------------------------------------------------------
/packages/frontend/public/180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/180x180.png
--------------------------------------------------------------------------------
/packages/frontend/public/57x57-no-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/57x57-no-bg.png
--------------------------------------------------------------------------------
/packages/frontend/public/57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/57x57.png
--------------------------------------------------------------------------------
/packages/frontend/public/72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/72x72.png
--------------------------------------------------------------------------------
/packages/frontend/public/76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/76x76.png
--------------------------------------------------------------------------------
/packages/frontend/public/brand-1200x600.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/brand-1200x600.png
--------------------------------------------------------------------------------
/packages/frontend/public/brand-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/brand-192x192.png
--------------------------------------------------------------------------------
/packages/frontend/public/brand-200x200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/brand-200x200.png
--------------------------------------------------------------------------------
/packages/frontend/public/brand-430x495.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/brand-430x495.png
--------------------------------------------------------------------------------
/packages/frontend/public/brand-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/brand-512x512.png
--------------------------------------------------------------------------------
/packages/frontend/public/brand-800x800.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/brand-800x800.png
--------------------------------------------------------------------------------
/packages/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/packages/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/packages/frontend/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | http://localhost:3000/404
10 | 2021-04-13
11 |
12 |
13 |
14 | http://localhost:3000/meeting
15 | 2021-04-13
16 |
17 |
18 |
19 | http://localhost:3000/
20 | 2021-04-13
21 |
22 |
--------------------------------------------------------------------------------
/packages/frontend/public/virtual-bgs/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technikhil314/next-webrtc/ad7dcb6a2b8bdc21efc46ddf4eb206f0f35f6126/packages/frontend/public/virtual-bgs/1.jpg
--------------------------------------------------------------------------------
/packages/frontend/sitemap-generator.js:
--------------------------------------------------------------------------------
1 | const sitemap = require("nextjs-sitemap-generator");
2 |
3 | sitemap({
4 | baseUrl: process.env.NEXT_PUBLIC_URL,
5 | pagesDirectory: __dirname + "/.next/serverless/pages",
6 | targetDirectory: "public/",
7 | extraPaths: ["/"],
8 | ignoredExtensions: ["js", "map"],
9 | ignoredPaths: ["assets", "[roomName]", "index"],
10 | nextConfigPath: __dirname + "/next.config.js",
11 | });
12 |
--------------------------------------------------------------------------------
/packages/frontend/store/states.js:
--------------------------------------------------------------------------------
1 | import createRhinoState from "react-rhino";
2 |
3 | const { RhinoProvider, useRhinoState } = createRhinoState({
4 | roomName: null,
5 | isStarted: false,
6 | shareScreen: false,
7 | userName: "",
8 | localStream: null,
9 | });
10 |
11 | export { RhinoProvider, useRhinoState };
12 |
--------------------------------------------------------------------------------
/packages/frontend/styles/globals.scss:
--------------------------------------------------------------------------------
1 | @import "../../../node_modules/modern-normalize/modern-normalize.css";
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | body,
7 | html {
8 | font-family: Roboto, sans-serif;
9 | background-color: #fff;
10 | height: 100%;
11 | }
12 |
13 | #__next {
14 | display: grid;
15 | grid-template-columns: 1fr;
16 | grid-template-rows: 60px 1fr;
17 | grid-template-areas:
18 | "nav"
19 | "body";
20 | min-height: 100%;
21 | }
22 |
23 | nav {
24 | grid-area: nav;
25 | }
26 |
27 | main {
28 | grid-area: body;
29 | min-height: calc(100vh - 60px - 2rem);
30 | z-index: 1;
31 | }
32 |
33 | .remote-video,
34 | .local-video {
35 | position: relative;
36 | &::after {
37 | position: absolute;
38 | bottom: 0;
39 | right: 0;
40 | padding: 5px 10px;
41 | width: min-content;
42 | background-color: black;
43 | color: white;
44 | border-radius: 10px 0 10px 0;
45 | content: attr(data-username);
46 | height: min-content;
47 | line-height: 1.1em;
48 | vertical-align: middle;
49 | text-align: center;
50 | z-index: 2;
51 | text-transform: capitalize;
52 | }
53 | }
54 |
55 | .local-video {
56 | position: sticky;
57 | top: 100%;
58 | left: 100%;
59 | }
60 |
61 | .ribbon {
62 | @apply bg-gray-900;
63 | width: 100px;
64 | height: 100px;
65 | position: fixed;
66 | top: 0;
67 | right: 0;
68 | clip-path: polygon(0 0, 100% 0, 100% 100%);
69 | svg {
70 | position: absolute;
71 | left: 65%;
72 | top: 20%;
73 | transform-origin: top center;
74 | transform: rotate(45deg);
75 | }
76 | }
77 |
78 | .footer {
79 | .uppercase {
80 | font-variant: small-caps;
81 | }
82 | }
83 |
84 | video {
85 | background-color: black;
86 | }
87 |
--------------------------------------------------------------------------------
/packages/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | darkMode: "class", // or 'media' or 'class'
3 | purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
4 | mode: "jit",
5 | theme: {
6 | extend: {
7 | gridAutoRows: {
8 | "350px": "350px",
9 | },
10 | zIndex: {
11 | "-1": -1,
12 | },
13 | translate: {
14 | "-screen": "-100vw",
15 | },
16 | scale: {
17 | "-1": "-1",
18 | },
19 | },
20 | },
21 | variants: {
22 | extend: {},
23 | },
24 | plugins: [],
25 | };
26 |
--------------------------------------------------------------------------------
/packages/frontend/utils/classNames.js:
--------------------------------------------------------------------------------
1 | export function classNames(classNameMap) {
2 | let classNameString = "";
3 | for (const className in classNameMap) {
4 | const condition = classNameMap[className];
5 | if (condition) {
6 | classNameString += ` ${className}`;
7 | }
8 | }
9 | return classNameString.trim();
10 | }
11 |
--------------------------------------------------------------------------------
/packages/frontend/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const text = {
2 | seoTagLine: "Bringing the web based video recording & privacy first video conferencing to the world.",
3 | titleDesc: "An open source alternative to video recording & video conferencing.",
4 | appName: "OpenRTC",
5 | };
6 |
7 | export const userMediaConstraints = {
8 | audio: {},
9 | video: {},
10 | };
11 |
12 | export const vlogRecordingVideoCodecType = {
13 | mimeType: "video/webm;",
14 | };
15 |
16 | export const iceConfig = {
17 | iceServers: [
18 | {
19 | urls: [
20 | "stun:stun.l.google.com:19302",
21 | "stun:stun.l.google.com:19302",
22 | "stun:stun1.l.google.com:19302",
23 | "stun:stun2.l.google.com:19302",
24 | "stun:stun3.l.google.com:19302",
25 | "stun:stun4.l.google.com:19302",
26 | "stun:stun.ekiga.net",
27 | "stun:stun.ideasip.com",
28 | "stun:stun.rixtelecom.se",
29 | "stun:stun.schlund.de",
30 | "stun:stun.stunprotocol.org:3478",
31 | "stun:stun.voiparound.com",
32 | "stun:stun.voipbuster.com",
33 | "stun:stun.voipstunt.com",
34 | "stun:stun.voxgratia.org",
35 | ],
36 | },
37 | {
38 | urls: "turn:13.250.13.83:3478?transport=udp",
39 | username: "YzYNCouZM1mhqhmseWk6",
40 | credential: "YzYNCouZM1mhqhmseWk6",
41 | },
42 | {
43 | urls: "turn:numb.viagenie.ca",
44 | credential: "muazkh",
45 | username: "webrtc@live.com",
46 | },
47 | {
48 | urls: "turn:192.158.29.39:3478?transport=udp",
49 | credential: "JZEOEt2V3Qb0y27GRntt2u2PAYA=",
50 | username: "28224511:1379330808",
51 | },
52 | {
53 | urls: "turn:192.158.29.39:3478?transport=tcp",
54 | credential: "JZEOEt2V3Qb0y27GRntt2u2PAYA=",
55 | username: "28224511:1379330808",
56 | },
57 | {
58 | urls: "turn:turn.bistri.com:80",
59 | credential: "homeo",
60 | username: "homeo",
61 | },
62 | {
63 | urls: "turn:turn.anyfirewall.com:443?transport=tcp",
64 | credential: "webrtc",
65 | username: "webrtc",
66 | },
67 | ],
68 | };
69 |
70 | export const browserRegexes = {
71 | chrome: /(chrome|omniweb|arora|[tizenoka]{5} ?browser)\/v?([\w\.]+)/i,
72 | edge: /edg(?:e|ios|a)?\/([\w\.]+)/i,
73 | firefox: /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i,
74 | };
75 |
--------------------------------------------------------------------------------
/packages/frontend/utils/helpers.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable-next-line */
2 | import * as tf from "@tensorflow/tfjs";
3 | import { browserRegexes } from "./constants";
4 | import * as bodyPix from "@tensorflow-models/body-pix";
5 |
6 | export function capitalize(s) {
7 | if (typeof s !== "string") return "";
8 | return s.charAt(0).toUpperCase() + s.slice(1);
9 | }
10 |
11 | export async function loadBodyPix() {
12 | const options = {
13 | architecture: "ResNet50",
14 | multiplier: 1,
15 | stride: 16,
16 | quantBytes: 4,
17 | };
18 | const net = await bodyPix.load(options);
19 | return net;
20 | }
21 | export function classNames(classNameMap) {
22 | let classNameString = "";
23 | for (const className in classNameMap) {
24 | const condition = classNameMap[className];
25 | if (condition) {
26 | classNameString += ` ${className}`;
27 | }
28 | }
29 | return classNameString.trim();
30 | }
31 |
32 | export function readAsObjectURL(file) {
33 | return new Promise((res, rej) => {
34 | const reader = new FileReader();
35 | reader.onload = (e) => {
36 | const blob = new Blob([e.target.result]);
37 | const url = URL.createObjectURL(blob);
38 | res(url);
39 | };
40 | reader.onerror = (e) => rej(e);
41 | reader.readAsArrayBuffer(file);
42 | });
43 | }
44 |
45 | export function getBrowserName(uaString) {
46 | const browserMatch = Object.entries(browserRegexes).find(([, regex]) => regex.test(uaString));
47 |
48 | if (!browserMatch) {
49 | return null;
50 | }
51 |
52 | const [browserName] = browserMatch;
53 | return browserName;
54 | }
55 |
56 | export function rgb2hsl(r, g, b) {
57 | r /= 255;
58 | g /= 255;
59 | b /= 255;
60 |
61 | var min = Math.min(r, g, b);
62 | var max = Math.max(r, g, b);
63 | var delta = max - min;
64 | var h, s, l;
65 |
66 | if (max === min) {
67 | h = 0;
68 | } else if (r === max) {
69 | h = (g - b) / delta;
70 | } else if (g === max) {
71 | h = 2 + (b - r) / delta;
72 | } else if (b === max) {
73 | h = 4 + (r - g) / delta;
74 | }
75 |
76 | h = Math.min(h * 60, 360);
77 |
78 | if (h < 0) {
79 | h += 360;
80 | }
81 |
82 | l = (min + max) / 2;
83 |
84 | if (max === min) {
85 | s = 0;
86 | } else if (l <= 0.5) {
87 | s = delta / (max + min);
88 | } else {
89 | s = delta / (2 - max - min);
90 | }
91 |
92 | return [h, s * 100, l * 100];
93 | }
94 |
95 | export function debug(...msg) {
96 | if (process.env.NEXT_PUBLIC_DEBUG) {
97 | console.debug(msg);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------