├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── dependabot.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── netlify.toml ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── sample.env ├── screenshot-react-demo.png └── src ├── CallObjectContext.js ├── api.js ├── components ├── App │ ├── App.css │ └── App.js ├── BrowserUnsupported │ ├── BrowserUnsupported.css │ └── BrowserUnsupported.js ├── Call │ ├── Call.css │ ├── Call.js │ └── callState.js ├── CallMessage │ ├── CallMessage.css │ └── CallMessage.js ├── Chat │ ├── Chat.css │ └── Chat.js ├── Icon │ └── Icon.js ├── StartButton │ ├── StartButton.css │ └── StartButton.js ├── Tile │ ├── Tile.css │ └── Tile.js ├── Tray │ ├── Tray.css │ └── Tray.js └── TrayButton │ ├── TrayButton.css │ └── TrayButton.js ├── index.css ├── index.js ├── logUtils.js ├── logo.svg └── urlUtils.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: BUG 5 | labels: bug 6 | assignees: kimberleejohnson 7 | --- 8 | 9 | 10 | 11 | # Expected behavior 12 | 13 | 14 | 15 | # Describe the bug (unexpected behavior) 16 | 17 | 18 | 19 | # Steps to reproduce 20 | 21 | 22 | 23 | 24 | 25 | 26 | # Screenshots 27 | 28 | 29 | 30 | # System information 31 | 32 | 33 | 34 | - Device: 35 | - OS, version: 36 | - Browser, version: 37 | 38 | # Additional context 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" # update frequency 7 | allow: 8 | - dependency-name: "@daily-co/daily-js" 9 | dependency-type: "direct" 10 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Local Netlify folder 26 | .netlify -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | unsafe-perm = true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # From .gitignore # 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 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 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | # Elastic Beanstalk Files 110 | .elasticbeanstalk/* 111 | !.elasticbeanstalk/*.cfg.yml 112 | !.elasticbeanstalk/*.global.yml 113 | 114 | # Elastic Beanstalk deployment created by GitHub Actions 115 | deploy.zip 116 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | help@daily.co. All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for looking into contributing to`daily-demos`! We want this repo to help people experiment with different Daily projects more quickly. We especially welcome any contributions that help us make existing demos easier to understand, improve demos' instructions and descriptions, and we're especially excited about any new demos that highlight unique ways to use the [Daily API](https://docs.daily.co/reference). 4 | 5 | **Before contributing:** 6 | 7 | - [Run daily-demos locally](#run-daily-demos-locally) 8 | - [Read our code of conduct](#read-our-code-of-conduct) 9 | 10 | **How to contribute:** 11 | 12 | - [Open or claim an issue](#open-or-claim-an-issue) 13 | - [Open a pull request](#open-a-pull-request) 14 | - [Contribute a new demo project](#contribute-a-new-demo-project) 15 | 16 | ## Before contributing 17 | 18 | ### Run call-object-react locally 19 | 20 | Please follow the instructions in `README.md`. 21 | 22 | ### Read our code of conduct 23 | 24 | We use the [Contributor Covenant](https://www.contributor-covenant.org/) for our Code of Conduct. Before contributing, [please read it](CODE_OF_CONDUCT.md). 25 | 26 | ## How to contribute 27 | 28 | ### Open or claim an issue 29 | 30 | #### Open an issue 31 | 32 | Today we work off two main issue templates: _bug reports_ and _demo/feature requests_. 33 | 34 | _Bug reports_ 35 | 36 | Before creating a new bug report, please do two things: 37 | 38 | 1. If you want to report a bug you experienced while on a Daily call, try out these [troubleshooting tips](https://help.daily.co/en/articles/2303117-top-troubleshooting-tips) to see if that takes care of the bug. 39 | 2. If you're still seeing the error, check to see if somebody else has [already filed the issue](https://github.com/daily-demos/call-object-react/issues) before creating a new one. 40 | 41 | If you've done those two things and need to create an issue, we'll ask you to tell us: 42 | 43 | - What you expected to happen 44 | - What actually happened 45 | - Steps to reproduce the error 46 | - Screenshots that illustrate where and what we should be looking for when we reproduce 47 | - System information, like your device, OS, and browser 48 | - Any additional context that you think could help us work through this 49 | 50 | _Demo/feature requests_ 51 | 52 | We're always happy to hear about new ways you'd like to use Daily. If you'd like a demo that we don't have yet, we'll ask you to let us know: 53 | 54 | - If the demo will help you solve a particular problem 55 | - Alternative solutions you've considered 56 | - Any additional context that might help us understand this ask 57 | 58 | #### Claim an issue 59 | 60 | All issues labeled `good-first-issue` are up for grabs. If you'd like to tackle an existing issue, feel free to assign yourself, and please leave a comment letting everyone know that you're on it. 61 | 62 | ### Open a pull request 63 | 64 | - If it's been a minute or if you haven't yet cloned, forked, or branched a repository, GitHub has some [docs to help](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests). 65 | - When creating commit messages and pull request titles, please follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) standard. 66 | 67 | ### Contribute a new demo project 68 | 69 | If you've built a project on Daily that you want to share with other developers, we'd be more than happy to help spread the word. 70 | 71 | To add a new demo project: 72 | 73 | Open a PR in [awesome-daily](#) and add a link to your project. 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A video chat app using React and the Daily JavaScript API 2 | 3 | ❗❗ **Note: As of July 2022, Daily's team recommends using the [custom-video-daily-react-hooks](https://github.com/daily-demos/custom-video-daily-react-hooks) demo app for sample code instead of this repo. It has a similar feature set (all features covered here are included, plus some extras!). Additionally, it showcases how to build a custom Daily video app using [Daily React Hooks](https://docs.daily.co/reference/daily-react-hooks), Daily's custom React hooks library.** 4 | 5 | **Daily recommends using [Daily React Hooks](https://docs.daily.co/reference/daily-react-hooks) for all custom React apps.** 6 | 7 | --- 8 | 9 | This demo is meant to showcase a basic but complete video chat web app using React and the low-level Daily call object. [The call object](https://docs.daily.co/docs/build-a-custom-video-chat-interface#daily-call-object) gives you direct access to the audio and video tracks, letting you build your app however you'd like with those primitives. 10 | 11 | ![Two participants on a video chat call](./screenshot-react-demo.png) 12 | 13 | For a step-by-step guide on how we built this demo, [check out our blog post](https://www.daily.co/blog/building-a-custom-video-chat-app-with-react/). 14 | 15 | Check out a live version of the demo [here](https://call-object-react.netlify.app/). 16 | 17 | ## Prerequisites 18 | 19 | - [Sign up for a Daily account](https://dashboard.daily.co/signup). 20 | - [Create a Daily room URL](https://help.daily.co/en/articles/4202139-creating-and-viewing-rooms) to test a video call quickly and hardcode a room URL (_this is NOT recommended for production_). 21 | 22 | ## How the demo works 23 | 24 | In our app, when a user clicks to start a call, the app will create a [meeting room](https://docs.daily.co/reference#rooms), pass the room’s URL to a new Daily call object, and join the call [0]. The call object is something that keeps track of important information about the meeting, like other participants (including their audio and video tracks) and the things they do on the call (e.g. muting their mic or leaving), and provides methods for interacting with the meeting. The app leverages this object to update its state accordingly, and to carry out user actions like muting or screen-sharing. When the user leaves the meeting room, the call object is destroyed. 25 | 26 | [0] If you'll be hardcoding the room URL for testing, the room will be passed as is to the call object. It bears repeating that _this is NOT recommended for production_. 27 | 28 | Please note this project is designed to work with rooms that have [privacy](https://www.daily.co/blog/intro-to-room-access-control/) set to `public`. If you are hardcoding a room URL, please bare in mind that token creation, pre-authorization and knock for access have not been implemented (meaning other participants may not be able to join your call) 29 | 30 | ## Running locally 31 | 32 | 1. Install dependencies `npm i` 33 | 2. Start dev server `npm run dev` 34 | 3. Then open your browser and go to `http://localhost:3002` 35 | 4. Add the Daily room URL you created to line 31 of `api.js`, and follow the comment's instructions. 36 | 37 | OR... 38 | 39 | ## Running using Netlify CLI 40 | 41 | If you want access to the Daily REST API (using the proxy as specified in `netlify.toml`) as well as a more robust local dev environment, please do the following (in this project's directory): 42 | 43 | 1. Deploy to your Netlify account 44 | [![Deploy with Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/daily-demos/call-object-react) 45 | 2. Install the Netlify CLI `npm i -g netlify-cli` 46 | 3. Login to your account `netlify login` 47 | 4. Rename `sample.env` to `.env` and add your API key 48 | 5. Start the dev server `netlify dev` 49 | 50 | > Note: If the API proxy isn't working locally you may need to run `netlify build` first. This will put API key in the `netlify.toml` file, so make sure you don't commit this change. 51 | 52 | ## Contributing and feedback 53 | 54 | Let us know how experimenting with this demo goes! Feel free to [open an Issue](https://github.com/daily-demos/call-object-react/issues), or reach us any time at `help@daily.co`. You can also join the conversation about this demo on [DEV](https://dev.to/trydaily/build-a-video-chat-app-in-minutes-with-react-and-daily-js-481c). 55 | 56 | ## What's next 57 | 58 | To get to know even more Daily API methods and events, explore our other demos, like [how to add your own chat interface](https://github.com/daily-co/daily-demos/tree/main/static-demos/simple-chat-demo). 59 | 60 | --- 61 | 62 | ## Related blog posts/tutorials 63 | 64 | Learn more about how to build your own video chat app in React using Daily with [this tutorial](https://www.daily.co/blog/building-a-custom-video-chat-app-with-react/). 65 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run netlify-build" 3 | publish = "build/" 4 | 5 | [build.environment] 6 | # suppress failures for for build warnings 7 | CI = "false" 8 | 9 | [template.environment] 10 | DAILY_API_KEY = "Replace with Daily API key" 11 | 12 | 13 | [[redirects]] 14 | # Proxies the Daily /rooms endpoint, POST will create a room and a GET will return a list 15 | # The placeholder below gets replaced when the build command runs 16 | # as suggested here: https://docs.netlify.com/configure-builds/file-based-configuration/#inject-environment-variable-values 17 | # IF YOU RUN THIS COMMAND LOCALLY DO NOT COMMIT THIS FILE WITH THE API KEY IN IT 18 | # MAKE SURE THE PLACEHOLDER TEXT IS THERE WHENEVER YOU ARE DONE TESTING LOCALLY 19 | from = "/api/rooms" 20 | to = "https://api.daily.co/v1/rooms" 21 | status = 200 22 | force = true 23 | headers = {Authorization = "Bearer DAILY_API_KEY_PLACEHOLDER"} 24 | 25 | # The following redirect is intended for use with most SPAs that handle routing internally. 26 | [[redirects]] 27 | from = "/*" 28 | to = "/index.html" 29 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@daily-co/daily-js": "^0.80.0", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^14.0.0", 9 | "@testing-library/user-event": "^14.4.3", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-scripts": "^5.0.1", 13 | "serve": "^14.2.0" 14 | }, 15 | "scripts": { 16 | "dev": "PORT=3002 react-scripts start", 17 | "build": "react-scripts build", 18 | "netlify-build": "sed -i s/DAILY_API_KEY_PLACEHOLDER/${DAILY_API_KEY}/g netlify.toml && npm run build", 19 | "start": "PORT=3002 serve build", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/call-object-react/be83520cfaace5b1493dd4d76a2283c9bd83521f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Daily React Demo 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/call-object-react/be83520cfaace5b1493dd4d76a2283c9bd83521f/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/call-object-react/be83520cfaace5b1493dd4d76a2283c9bd83521f/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | DAILY_API_KEY=REPLACE_WITH_YOUR_API_KEY -------------------------------------------------------------------------------- /screenshot-react-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/call-object-react/be83520cfaace5b1493dd4d76a2283c9bd83521f/screenshot-react-demo.png -------------------------------------------------------------------------------- /src/CallObjectContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createContext(); 4 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const newRoomEndpoint = `${window.location.origin}/api/rooms`; 2 | 3 | /** 4 | * Create a short-lived room for demo purposes. 5 | * 6 | * It uses the redirect proxy as specified in netlify.toml` 7 | * This will work locally if you following the Netlify specific instructions 8 | * in README.md 9 | * 10 | * See https://docs.daily.co/reference#create-room for more information on how 11 | * to use the Daily REST API to create rooms and what options are available. 12 | */ 13 | async function createRoom() { 14 | const exp = Math.round(Date.now() / 1000) + 60 * 30; 15 | const options = { 16 | properties: { 17 | exp: exp, 18 | }, 19 | }; 20 | let response = await fetch(newRoomEndpoint, { 21 | method: 'POST', 22 | body: JSON.stringify(options), 23 | mode: 'cors', 24 | }), 25 | room = await response.json(); 26 | return room; 27 | 28 | // Comment out the above and uncomment the below, using your own URL 29 | // return { url: 'https://your-domain.daily.co/hello' } 30 | } 31 | 32 | export default { createRoom }; 33 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | background-color: #4a4a4a; 3 | position: absolute; 4 | height: 100%; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react'; 2 | import Call from '../Call/Call'; 3 | import StartButton from '../StartButton/StartButton'; 4 | import api from '../../api'; 5 | import './App.css'; 6 | import Tray from '../Tray/Tray'; 7 | import CallObjectContext from '../../CallObjectContext'; 8 | import { roomUrlFromPageUrl, pageUrlFromRoomUrl } from '../../urlUtils'; 9 | import DailyIframe from '@daily-co/daily-js'; 10 | import { logDailyEvent } from '../../logUtils'; 11 | 12 | const STATE_IDLE = 'STATE_IDLE'; 13 | const STATE_CREATING = 'STATE_CREATING'; 14 | const STATE_JOINING = 'STATE_JOINING'; 15 | const STATE_JOINED = 'STATE_JOINED'; 16 | const STATE_LEAVING = 'STATE_LEAVING'; 17 | const STATE_ERROR = 'STATE_ERROR'; 18 | 19 | export default function App() { 20 | const [appState, setAppState] = useState(STATE_IDLE); 21 | const [roomUrl, setRoomUrl] = useState(null); 22 | const [callObject, setCallObject] = useState(null); 23 | 24 | /** 25 | * Creates a new call room. 26 | */ 27 | const createCall = useCallback(() => { 28 | setAppState(STATE_CREATING); 29 | return api 30 | .createRoom() 31 | .then((room) => room.url) 32 | .catch((error) => { 33 | console.log('Error creating room', error); 34 | setRoomUrl(null); 35 | setAppState(STATE_IDLE); 36 | }); 37 | }, []); 38 | 39 | /** 40 | * Starts joining an existing call. 41 | * 42 | * NOTE: In this demo we show how to completely clean up a call with destroy(), 43 | * which requires creating a new call object before you can join() again. 44 | * This isn't strictly necessary, but is good practice when you know you'll 45 | * be done with the call object for a while and you're no longer listening to its 46 | * events. 47 | */ 48 | const startJoiningCall = useCallback((url) => { 49 | const newCallObject = DailyIframe.createCallObject(); 50 | setRoomUrl(url); 51 | setCallObject(newCallObject); 52 | setAppState(STATE_JOINING); 53 | newCallObject.join({ url }); 54 | }, []); 55 | 56 | /** 57 | * Starts leaving the current call. 58 | */ 59 | const startLeavingCall = useCallback(() => { 60 | if (!callObject) return; 61 | // If we're in the error state, we've already "left", so just clean up 62 | if (appState === STATE_ERROR) { 63 | callObject.destroy().then(() => { 64 | setRoomUrl(null); 65 | setCallObject(null); 66 | setAppState(STATE_IDLE); 67 | }); 68 | } else { 69 | setAppState(STATE_LEAVING); 70 | callObject.leave(); 71 | } 72 | }, [callObject, appState]); 73 | 74 | /** 75 | * If a room's already specified in the page's URL when the component mounts, 76 | * join the room. 77 | */ 78 | useEffect(() => { 79 | const url = roomUrlFromPageUrl(); 80 | url && startJoiningCall(url); 81 | }, [startJoiningCall]); 82 | 83 | /** 84 | * Update the page's URL to reflect the active call when roomUrl changes. 85 | * 86 | * This demo uses replaceState rather than pushState in order to avoid a bit 87 | * of state-management complexity. See the comments around enableCallButtons 88 | * and enableStartButton for more information. 89 | */ 90 | useEffect(() => { 91 | const pageUrl = pageUrlFromRoomUrl(roomUrl); 92 | if (pageUrl === window.location.href) return; 93 | window.history.replaceState(null, null, pageUrl); 94 | }, [roomUrl]); 95 | 96 | /** 97 | * Uncomment to attach call object to window for debugging purposes. 98 | */ 99 | // useEffect(() => { 100 | // window.callObject = callObject; 101 | // }, [callObject]); 102 | 103 | /** 104 | * Update app state based on reported meeting state changes. 105 | * 106 | * NOTE: Here we're showing how to completely clean up a call with destroy(). 107 | * This isn't strictly necessary between join()s, but is good practice when 108 | * you know you'll be done with the call object for a while and you're no 109 | * longer listening to its events. 110 | */ 111 | useEffect(() => { 112 | if (!callObject) return; 113 | 114 | const events = ['joined-meeting', 'left-meeting', 'error']; 115 | 116 | function handleNewMeetingState(event) { 117 | event && logDailyEvent(event); 118 | switch (callObject.meetingState()) { 119 | case 'joined-meeting': 120 | setAppState(STATE_JOINED); 121 | break; 122 | case 'left-meeting': 123 | callObject.destroy().then(() => { 124 | setRoomUrl(null); 125 | setCallObject(null); 126 | setAppState(STATE_IDLE); 127 | }); 128 | break; 129 | case 'error': 130 | setAppState(STATE_ERROR); 131 | break; 132 | default: 133 | break; 134 | } 135 | } 136 | 137 | // Use initial state 138 | handleNewMeetingState(); 139 | 140 | // Listen for changes in state 141 | for (const event of events) { 142 | callObject.on(event, handleNewMeetingState); 143 | } 144 | 145 | // Stop listening for changes in state 146 | return function cleanup() { 147 | for (const event of events) { 148 | callObject.off(event, handleNewMeetingState); 149 | } 150 | }; 151 | }, [callObject]); 152 | 153 | /** 154 | * Listen for app messages from other call participants. 155 | */ 156 | useEffect(() => { 157 | if (!callObject) { 158 | return; 159 | } 160 | 161 | function handleAppMessage(event) { 162 | if (event) { 163 | logDailyEvent(event); 164 | console.log(`received app message from ${event.fromId}: `, event.data); 165 | } 166 | } 167 | 168 | callObject.on('app-message', handleAppMessage); 169 | 170 | return function cleanup() { 171 | callObject.off('app-message', handleAppMessage); 172 | }; 173 | }, [callObject]); 174 | 175 | /** 176 | * Show the call UI if we're either joining, already joined, or are showing 177 | * an error. 178 | */ 179 | const showCall = [STATE_JOINING, STATE_JOINED, STATE_ERROR].includes( 180 | appState 181 | ); 182 | 183 | /** 184 | * Only enable the call buttons (camera toggle, leave call, etc.) if we're joined 185 | * or if we've errored out. 186 | * 187 | * !!! 188 | * IMPORTANT: calling callObject.destroy() *before* we get the "joined-meeting" 189 | * can result in unexpected behavior. Disabling the leave call button 190 | * until then avoids this scenario. 191 | * !!! 192 | */ 193 | const enableCallButtons = [STATE_JOINED, STATE_ERROR].includes(appState); 194 | 195 | /** 196 | * Only enable the start button if we're in an idle state (i.e. not creating, 197 | * joining, etc.). 198 | * 199 | * !!! 200 | * IMPORTANT: only one call object is meant to be used at a time. Creating a 201 | * new call object with DailyIframe.createCallObject() *before* your previous 202 | * callObject.destroy() completely finishes can result in unexpected behavior. 203 | * Disabling the start button until then avoids that scenario. 204 | * !!! 205 | */ 206 | const enableStartButton = appState === STATE_IDLE; 207 | 208 | return ( 209 |
210 | {showCall ? ( 211 | // NOTE: for an app this size, it's not obvious that using a Context 212 | // is the best choice. But for larger apps with deeply-nested components 213 | // that want to access call object state and bind event listeners to the 214 | // call object, this can be a helpful pattern. 215 | 216 | 217 | 221 | 222 | ) : ( 223 | { 226 | createCall().then((url) => startJoiningCall(url)); 227 | }} 228 | /> 229 | )} 230 |
231 | ); 232 | } 233 | -------------------------------------------------------------------------------- /src/components/BrowserUnsupported/BrowserUnsupported.css: -------------------------------------------------------------------------------- 1 | .browser-unsupported { 2 | position: absolute; 3 | background: #ffffff; 4 | font-family: Helvetica Neue; 5 | font-size: 14px; 6 | line-height: 17px; 7 | text-align: center; 8 | color: #4a4a4a; 9 | top: 50%; 10 | left: 50%; 11 | transform: translate(-50%, -50%); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/BrowserUnsupported/BrowserUnsupported.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './BrowserUnsupported.css'; 3 | 4 | export default function BrowserUnsupported() { 5 | return ( 6 |

7 | Looks like you need to upgrade your browser to make Daily video calls. 8 |
9 | See  10 | this page 11 |  for help getting on a supported browser version. 12 |

13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Call/Call.css: -------------------------------------------------------------------------------- 1 | .call { 2 | position: relative; /* To make it a "positioned" container so children layout works */ 3 | height: calc(100% - 60px); /* Space for the tray */ 4 | } 5 | 6 | .large-tiles { 7 | height: calc(100% - 132.5px); 8 | width: 80%; 9 | position: relative; 10 | left: 50%; 11 | transform: translate(-50%, 0); 12 | display: grid; 13 | grid-template-columns: repeat(auto-fit, minmax(30%, 1fr)); 14 | align-items: center; 15 | overflow-y: scroll; 16 | } 17 | 18 | .small-tiles { 19 | height: 132.5px; /* Video height + 10px padding either side */ 20 | display: flex; 21 | align-items: center; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Call/Call.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useReducer, useCallback } from 'react'; 2 | import './Call.css'; 3 | import Tile from '../Tile/Tile'; 4 | import CallObjectContext from '../../CallObjectContext'; 5 | import CallMessage from '../CallMessage/CallMessage'; 6 | import { 7 | initialCallState, 8 | CLICK_ALLOW_TIMEOUT, 9 | PARTICIPANTS_CHANGE, 10 | CAM_OR_MIC_ERROR, 11 | FATAL_ERROR, 12 | callReducer, 13 | isLocal, 14 | isScreenShare, 15 | containsScreenShare, 16 | getMessage, 17 | } from './callState'; 18 | import { logDailyEvent } from '../../logUtils'; 19 | 20 | export default function Call() { 21 | const callObject = useContext(CallObjectContext); 22 | const [callState, dispatch] = useReducer(callReducer, initialCallState); 23 | 24 | /** 25 | * Start listening for participant changes, when the callObject is set. 26 | */ 27 | useEffect(() => { 28 | if (!callObject) return; 29 | 30 | const events = [ 31 | 'participant-joined', 32 | 'participant-updated', 33 | 'participant-left', 34 | ]; 35 | 36 | function handleNewParticipantsState(event) { 37 | event && logDailyEvent(event); 38 | dispatch({ 39 | type: PARTICIPANTS_CHANGE, 40 | participants: callObject.participants(), 41 | }); 42 | } 43 | 44 | // Use initial state 45 | handleNewParticipantsState(); 46 | 47 | // Listen for changes in state 48 | for (const event of events) { 49 | callObject.on(event, handleNewParticipantsState); 50 | } 51 | 52 | // Stop listening for changes in state 53 | return function cleanup() { 54 | for (const event of events) { 55 | callObject.off(event, handleNewParticipantsState); 56 | } 57 | }; 58 | }, [callObject]); 59 | 60 | /** 61 | * Start listening for call errors, when the callObject is set. 62 | */ 63 | useEffect(() => { 64 | if (!callObject) return; 65 | 66 | function handleCameraErrorEvent(event) { 67 | logDailyEvent(event); 68 | dispatch({ 69 | type: CAM_OR_MIC_ERROR, 70 | message: 71 | (event && event.errorMsg && event.errorMsg.errorMsg) || 'Unknown', 72 | }); 73 | } 74 | 75 | // We're making an assumption here: there is no camera error when callObject 76 | // is first assigned. 77 | 78 | callObject.on('camera-error', handleCameraErrorEvent); 79 | 80 | return function cleanup() { 81 | callObject.off('camera-error', handleCameraErrorEvent); 82 | }; 83 | }, [callObject]); 84 | 85 | /** 86 | * Start listening for fatal errors, when the callObject is set. 87 | */ 88 | useEffect(() => { 89 | if (!callObject) return; 90 | 91 | function handleErrorEvent(e) { 92 | logDailyEvent(e); 93 | dispatch({ 94 | type: FATAL_ERROR, 95 | message: (e && e.errorMsg) || 'Unknown', 96 | }); 97 | } 98 | 99 | // We're making an assumption here: there is no error when callObject is 100 | // first assigned. 101 | 102 | callObject.on('error', handleErrorEvent); 103 | 104 | return function cleanup() { 105 | callObject.off('error', handleErrorEvent); 106 | }; 107 | }, [callObject]); 108 | 109 | /** 110 | * Start a timer to show the "click allow" message, when the component mounts. 111 | */ 112 | useEffect(() => { 113 | const t = setTimeout(() => { 114 | dispatch({ type: CLICK_ALLOW_TIMEOUT }); 115 | }, 2500); 116 | 117 | return function cleanup() { 118 | clearTimeout(t); 119 | }; 120 | }, []); 121 | 122 | /** 123 | * Send an app message to the remote participant whose tile was clicked on. 124 | */ 125 | const sendHello = useCallback( 126 | (participantId) => { 127 | callObject && 128 | callObject.sendAppMessage({ hello: 'world' }, participantId); 129 | }, 130 | [callObject] 131 | ); 132 | 133 | function getTiles() { 134 | let largeTiles = []; 135 | let smallTiles = []; 136 | Object.entries(callState.callItems).forEach(([id, callItem]) => { 137 | const isLarge = 138 | isScreenShare(id) || 139 | (!isLocal(id) && !containsScreenShare(callState.callItems)); 140 | const tile = ( 141 | { 152 | sendHello(id); 153 | } 154 | } 155 | /> 156 | ); 157 | if (isLarge) { 158 | largeTiles.push(tile); 159 | } else { 160 | smallTiles.push(tile); 161 | } 162 | }); 163 | return [largeTiles, smallTiles]; 164 | } 165 | 166 | const [largeTiles, smallTiles] = getTiles(); 167 | const message = getMessage(callState); 168 | return ( 169 |
170 |
171 | { 172 | !message 173 | ? largeTiles 174 | : null /* Avoid showing large tiles to make room for the message */ 175 | } 176 |
177 |
{smallTiles}
178 | {message && ( 179 | 184 | )} 185 |
186 | ); 187 | } 188 | -------------------------------------------------------------------------------- /src/components/Call/callState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Call state is comprised of: 3 | * - "Call items" (inputs to the call, i.e. participants or shared screens) 4 | * - UI state that depends on call items (for now, just whether to show "click allow" message) 5 | * 6 | * Call items are keyed by id: 7 | * - "local" for the current participant 8 | * - A session id for each remote participant 9 | * - "-screen" for each shared screen 10 | */ 11 | const initialCallState = { 12 | callItems: { 13 | local: { 14 | videoTrackState: null, 15 | audioTrackState: null, 16 | }, 17 | }, 18 | clickAllowTimeoutFired: false, 19 | camOrMicError: null, 20 | fatalError: null, 21 | }; 22 | 23 | // --- Actions --- 24 | 25 | /** 26 | * CLICK_ALLOW_TIMEOUT action structure: 27 | * - type: string 28 | */ 29 | const CLICK_ALLOW_TIMEOUT = 'CLICK_ALLOW_TIMEOUT'; 30 | 31 | /** 32 | * PARTICIPANTS_CHANGE action structure: 33 | * - type: string 34 | * - participants: Object (from Daily callObject.participants()) 35 | */ 36 | const PARTICIPANTS_CHANGE = 'PARTICIPANTS_CHANGE'; 37 | 38 | /** 39 | * CAM_OR_MIC_ERROR action structure: 40 | * - type: string 41 | * - message: string 42 | */ 43 | const CAM_OR_MIC_ERROR = 'CAM_OR_MIC_ERROR'; 44 | 45 | /** 46 | * CAM_OR_MIC_ERROR action structure: 47 | * - type: string 48 | * - message: string 49 | */ 50 | const FATAL_ERROR = 'FATAL_ERROR'; 51 | 52 | // --- Reducer and helpers -- 53 | 54 | function callReducer(callState, action) { 55 | switch (action.type) { 56 | case CLICK_ALLOW_TIMEOUT: 57 | return { 58 | ...callState, 59 | clickAllowTimeoutFired: true, 60 | }; 61 | case PARTICIPANTS_CHANGE: 62 | const callItems = getCallItems(action.participants); 63 | return { 64 | ...callState, 65 | callItems, 66 | }; 67 | case CAM_OR_MIC_ERROR: 68 | return { ...callState, camOrMicError: action.message }; 69 | case FATAL_ERROR: 70 | return { ...callState, fatalError: action.message }; 71 | default: 72 | throw new Error(); 73 | } 74 | } 75 | 76 | function getLocalCallItem(callItems) { 77 | return callItems['local']; 78 | } 79 | 80 | function getCallItems(participants) { 81 | let callItems = { ...initialCallState.callItems }; // Ensure we *always* have a local participant 82 | for (const [id, participant] of Object.entries(participants)) { 83 | callItems[id] = { 84 | videoTrackState: participant.tracks.video, 85 | audioTrackState: participant.tracks.audio, 86 | }; 87 | if (shouldIncludeScreenCallItem(participant)) { 88 | callItems[id + '-screen'] = { 89 | videoTrackState: participant.tracks.screenVideo, 90 | audioTrackState: participant.tracks.screenAudio, 91 | }; 92 | } 93 | } 94 | return callItems; 95 | } 96 | 97 | function shouldIncludeScreenCallItem(participant) { 98 | const trackStatesForInclusion = ['loading', 'playable', 'interrupted']; 99 | return ( 100 | trackStatesForInclusion.includes(participant.tracks.screenVideo.state) || 101 | trackStatesForInclusion.includes(participant.tracks.screenAudio.state) 102 | ); 103 | } 104 | 105 | // --- Derived data --- 106 | 107 | // True if id corresponds to local participant (*not* their screen share) 108 | function isLocal(id) { 109 | return id === 'local'; 110 | } 111 | 112 | function isScreenShare(id) { 113 | return id.endsWith('-screen'); 114 | } 115 | 116 | function containsScreenShare(callItems) { 117 | return Object.keys(callItems).some((id) => isScreenShare(id)); 118 | } 119 | 120 | function getMessage(callState) { 121 | function shouldShowClickAllow() { 122 | const localCallItem = getLocalCallItem(callState.callItems); 123 | const hasLoaded = localCallItem && !localCallItem.isLoading; 124 | return !hasLoaded && callState.clickAllowTimeoutFired; 125 | } 126 | 127 | let header = null; 128 | let detail = null; 129 | let isError = false; 130 | if (callState.fatalError) { 131 | header = `Fatal error: ${callState.fatalError}`; 132 | isError = true; 133 | } else if (callState.camOrMicError) { 134 | header = `Camera or mic access error: ${callState.camOrMicError}`; 135 | detail = 136 | 'See https://help.daily.co/en/articles/2528184-unblock-camera-mic-access-on-a-computer to troubleshoot.'; 137 | isError = true; 138 | } else if (shouldShowClickAllow()) { 139 | header = 'Click "Allow" to enable camera and mic access'; 140 | } else if (Object.keys(callState.callItems).length === 1) { 141 | header = "Copy and share this page's URL to invite others"; 142 | detail = window.location.href; 143 | } 144 | return header || detail ? { header, detail, isError } : null; 145 | } 146 | 147 | export { 148 | initialCallState, 149 | CLICK_ALLOW_TIMEOUT, 150 | PARTICIPANTS_CHANGE, 151 | CAM_OR_MIC_ERROR, 152 | FATAL_ERROR, 153 | callReducer, 154 | isLocal, 155 | isScreenShare, 156 | containsScreenShare, 157 | getMessage, 158 | }; 159 | -------------------------------------------------------------------------------- /src/components/CallMessage/CallMessage.css: -------------------------------------------------------------------------------- 1 | .call-message { 2 | width: auto; 3 | padding: 20px 30px; 4 | top: 50%; 5 | left: 50%; 6 | position: absolute; 7 | transform: translate(-50%, -50%); 8 | color: #ffffff; 9 | text-align: center; 10 | font-size: 14px; 11 | line-height: 17px; 12 | } 13 | 14 | .call-message.error { 15 | color: #d81a1a; 16 | background-color: #ffffff; 17 | } 18 | 19 | .call-message-header { 20 | font-weight: bold; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/CallMessage/CallMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './CallMessage.css'; 3 | 4 | /** 5 | * Props: 6 | * - header: string 7 | * - detail: string 8 | * - isError: boolean 9 | */ 10 | export default function CallMessage(props) { 11 | return ( 12 |
13 |

{props.header}

14 |

{props.detail}

15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Chat/Chat.css: -------------------------------------------------------------------------------- 1 | .chat { 2 | position: absolute; 3 | right: 10px; 4 | bottom: 75px; 5 | width: 250px; 6 | height: calc(100% - 150px); 7 | background-color: #f2f2f2; 8 | border-radius: 4px; 9 | } 10 | 11 | .messageList { 12 | padding: 10px; 13 | } 14 | 15 | .chat-input { 16 | position: absolute; 17 | bottom: 0px; 18 | width: 200px; 19 | height: 25px; 20 | } 21 | 22 | .send-chat-button { 23 | position: absolute; 24 | bottom: 0px; 25 | right: 0px; 26 | width: 50px; 27 | height: 31px; 28 | background-color: #f2f2f2; 29 | font-weight: bold; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Chat/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import CallObjectContext from '../../CallObjectContext'; 3 | import './Chat.css'; 4 | 5 | export default function Chat(props) { 6 | const callObject = useContext(CallObjectContext); 7 | const [inputValue, setInputValue] = useState(''); 8 | const [chatHistory, setChatHistory] = useState([]); 9 | 10 | const handleChange = (event) => { 11 | setInputValue(event.target.value); 12 | }; 13 | 14 | function handleSubmit(event) { 15 | event.preventDefault(); 16 | callObject.sendAppMessage({ message: inputValue }, '*'); 17 | const name = callObject.participants().local.user_name 18 | ? callObject.participants().local.user_name 19 | : 'Guest'; 20 | setChatHistory([ 21 | ...chatHistory, 22 | { 23 | sender: name, 24 | message: inputValue, 25 | }, 26 | ]); 27 | setInputValue(''); 28 | } 29 | 30 | /** 31 | * Update chat state based on a message received to all participants. 32 | * 33 | */ 34 | useEffect(() => { 35 | if (!callObject) { 36 | return; 37 | } 38 | 39 | function handleAppMessage(event) { 40 | const participants = callObject.participants(); 41 | const name = participants[event.fromId].user_name 42 | ? participants[event.fromId].user_name 43 | : 'Guest'; 44 | setChatHistory([ 45 | ...chatHistory, 46 | { 47 | sender: name, 48 | message: event.data.message, 49 | }, 50 | ]); 51 | // Make other icons light up 52 | props.notification(); 53 | } 54 | 55 | callObject.on('app-message', handleAppMessage); 56 | 57 | return function cleanup() { 58 | callObject.off('app-message', handleAppMessage); 59 | }; 60 | }, [callObject, chatHistory]); 61 | 62 | useEffect(() => {}, [chatHistory]); 63 | 64 | return props.onClickDisplay ? ( 65 |
66 | {chatHistory.map((entry, index) => ( 67 |
68 | {entry.sender}: {entry.message} 69 |
70 | ))} 71 |
72 | 73 | 81 | 84 |
85 |
86 | ) : null; 87 | } 88 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TYPE_MUTE_CAMERA = 'camera'; 4 | const TYPE_MUTE_MIC = 'mute-mic'; 5 | const TYPE_SCREEN = 'screen'; 6 | const TYPE_LEAVE = 'leave'; 7 | const TYPE_CHAT = 'chat'; 8 | 9 | /** 10 | * Props: 11 | * - type: string 12 | * - highlighted: boolean 13 | */ 14 | export default function Icon(props) { 15 | function getFillColor() { 16 | return props.highlighted ? '#fb5554' : '#000000'; 17 | } 18 | 19 | function getPath() { 20 | switch (props.type) { 21 | case TYPE_MUTE_CAMERA: 22 | return ( 23 | 30 | 35 | 39 | 43 | 44 | 45 | 46 | ); 47 | case TYPE_MUTE_MIC: 48 | return ( 49 | 56 | 61 | 65 | 69 | 70 | 71 | 72 | ); 73 | case TYPE_SCREEN: 74 | return ( 75 | 80 | ); 81 | case TYPE_LEAVE: 82 | return ( 83 | 88 | ); 89 | case TYPE_CHAT: 90 | return ( 91 | 96 | ); 97 | default: 98 | throw new Error(); 99 | } 100 | } 101 | 102 | return ( 103 | 110 | {getPath()} 111 | 112 | ); 113 | } 114 | 115 | export { TYPE_MUTE_CAMERA, TYPE_MUTE_MIC, TYPE_SCREEN, TYPE_LEAVE, TYPE_CHAT }; 116 | -------------------------------------------------------------------------------- /src/components/StartButton/StartButton.css: -------------------------------------------------------------------------------- 1 | .start-button { 2 | padding: 20px 30px; 3 | position: absolute; 4 | background: #ffffff; 5 | font-family: Helvetica Neue; 6 | font-style: normal; 7 | font-weight: normal; 8 | font-size: 14px; 9 | line-height: 17px; 10 | text-align: center; 11 | color: #4a4a4a; 12 | top: 50%; 13 | left: 50%; 14 | transform: translate(-50%, -50%); 15 | background-color: #ffffff; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/StartButton/StartButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './StartButton.css'; 3 | 4 | /** 5 | * Props: 6 | * - disabled: boolean 7 | * - onClick: () => () 8 | */ 9 | export default function StartButton(props) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Tile/Tile.css: -------------------------------------------------------------------------------- 1 | .tile.small { 2 | width: 200px; 3 | margin: 0 10px; 4 | position: relative; 5 | } 6 | 7 | .tile.large { 8 | position: relative; 9 | margin: 2px; 10 | } 11 | 12 | .tile video { 13 | width: 100%; 14 | position: absolute; 15 | top: 0px; 16 | } 17 | 18 | .tile .background { 19 | background-color: #000000; 20 | width: 100%; 21 | padding-top: 56.25%; /* Hard-coded 16:9 aspect ratio */ 22 | } 23 | 24 | .tile.local video { 25 | transform: scale(-1, 1); 26 | } 27 | 28 | .tile.small video { 29 | border-radius: 4px; 30 | } 31 | 32 | .tile.small .background { 33 | border-radius: 4px; 34 | } 35 | 36 | .tile .overlay { 37 | position: absolute; 38 | color: #ffffff; 39 | top: 50%; 40 | left: 50%; 41 | margin: 0; 42 | transform: translate(-50%, -50%); 43 | font-size: 14px; 44 | line-height: 17px; 45 | } 46 | 47 | .tile .corner { 48 | position: absolute; 49 | color: #ffffff; 50 | background-color: #000000; 51 | padding: 10px; 52 | margin: 0; 53 | bottom: 0; 54 | left: 0; 55 | font-size: 14px; 56 | line-height: 17px; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Tile/Tile.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef } from 'react'; 2 | import './Tile.css'; 3 | 4 | function getTrackUnavailableMessage(kind, trackState) { 5 | if (!trackState) return; 6 | switch (trackState.state) { 7 | case 'blocked': 8 | if (trackState.blocked.byPermissions) { 9 | return `${kind} permission denied`; 10 | } else if (trackState.blocked.byDeviceMissing) { 11 | return `${kind} device missing`; 12 | } 13 | return `${kind} blocked`; 14 | case 'off': 15 | if (trackState.off.byUser) { 16 | return `${kind} muted`; 17 | } else if (trackState.off.byBandwidth) { 18 | return `${kind} muted to save bandwidth`; 19 | } 20 | return `${kind} off`; 21 | case 'sendable': 22 | return `${kind} not subscribed`; 23 | case 'loading': 24 | return `${kind} loading...`; 25 | case 'interrupted': 26 | return `${kind} interrupted`; 27 | case 'playable': 28 | return null; 29 | } 30 | } 31 | 32 | /** 33 | * Props 34 | * - videoTrackState: DailyTrackState? 35 | * - audioTrackState: DailyTrackState? 36 | * - isLocalPerson: boolean 37 | * - isLarge: boolean 38 | * - disableCornerMessage: boolean 39 | * - onClick: Function 40 | */ 41 | export default function Tile(props) { 42 | const videoEl = useRef(null); 43 | const audioEl = useRef(null); 44 | 45 | const videoTrack = useMemo(() => { 46 | // For video let's use the `track` field, which is only present when video 47 | // is in the "playable" state. 48 | // (Using `persistentTrack` could result in a still frame being shown when 49 | // remote video is muted). 50 | return props.videoTrackState?.track; 51 | }, [props.videoTrackState]); 52 | 53 | const audioTrack = useMemo(() => { 54 | // For audio let's use the `persistentTrack` field, which is present whether 55 | // or not audio is in the "playable" state. 56 | // (Using `track` would result in a bug where, if remote audio were unmuted 57 | // while this call was is in a Safari background tab, audio wouldn't resume 58 | // playing). 59 | return props.audioTrackState?.persistentTrack; 60 | }, [props.audioTrackState]); 61 | 62 | const videoUnavailableMessage = useMemo(() => { 63 | return getTrackUnavailableMessage('video', props.videoTrackState); 64 | }, [props.videoTrackState]); 65 | 66 | const audioUnavailableMessage = useMemo(() => { 67 | return getTrackUnavailableMessage('audio', props.audioTrackState); 68 | }, [props.audioTrackState]); 69 | 70 | /** 71 | * When video track changes, update video srcObject 72 | */ 73 | useEffect(() => { 74 | videoEl.current && 75 | (videoEl.current.srcObject = videoTrack && new MediaStream([videoTrack])); 76 | }, [videoTrack]); 77 | 78 | /** 79 | * When audio track changes, update audio srcObject 80 | */ 81 | useEffect(() => { 82 | audioEl.current && 83 | (audioEl.current.srcObject = audioTrack && new MediaStream([audioTrack])); 84 | }, [audioTrack]); 85 | 86 | function getVideoComponent() { 87 | return videoTrack &&