From 449bf6709964677c5b548375febdacaf330b2cd4 Mon Sep 17 00:00:00 2001 From: Chen Hui Jing Date: Wed, 22 Apr 2020 11:12:23 +0800 Subject: [PATCH] Initial commit --- .env.example | 3 + .gitignore | 115 ++++++++++++++++++++++++++ CODE_OF_CONDUCT.md | 80 ++++++++++++++++++ CONTRIBUTING.md | 121 +++++++++++++++++++++++++++ LICENSE | 21 +++++ PULL_REQUEST_TEMPLATE.md | 29 +++++++ README.md | 35 ++++++++ package.json | 28 +++++++ public/broadcast.js | 31 +++++++ public/client.js | 173 +++++++++++++++++++++++++++++++++++++++ public/style.css | 100 ++++++++++++++++++++++ server.js | 113 +++++++++++++++++++++++++ views/broadcast.html | 43 ++++++++++ views/index.html | 54 ++++++++++++ views/landing.html | 59 +++++++++++++ 15 files changed, 1005 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 PULL_REQUEST_TEMPLATE.md create mode 100644 README.md create mode 100644 package.json create mode 100644 public/broadcast.js create mode 100644 public/client.js create mode 100644 public/style.css create mode 100644 server.js create mode 100644 views/broadcast.html create mode 100644 views/index.html create mode 100644 views/landing.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ef7feb6 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +API_KEY='' +API_SECRET='' +PORT= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a02f2e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e482eed --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,80 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [devrel@vonage.com][devrel]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][cchomepage], version 1.4, +available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][cccoc] + +For answers to common questions about this code of conduct, see +[https://www.contributor-covenant.org/faq][ccfaq] + +[cccoc]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html "Contributor Covenant CoC" +[cchomepage]: https://www.contributor-covenant.org "Contributor Covenant" +[ccfaq]: https://www.contributor-covenant.org/faq "Contributor Covenant FAQ" + +[devrel]: devrel@vonage.com "DevRel at Vonage dot com" \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..54e3c1c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,121 @@ +# Contributing Guidelines + +:+1::tada: We :heart: contributions from everyone! :tada::+1: + +It is a good idea to [talk to us][slack] first if you plan to add any new functionality. Otherwise, [bug reports][issues], [bug fixes][pulls] and feedback on the library is always appreciated. Check out the [Contributing Guidelines][contributing] for more information and please follow the [GitHub Flow][githubflow]. + +[![contributions welcome][contribadge]][issues] + +The following is a set of guidelines for contributing to this project, which are hosted on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +Please take the time to review the [Code of Conduct][coc], which all contributors are subject to on this project. + +**Table of Contents** + +- [Reporting Bugs](#reporting-bugs) + - [Before Submitting A Bug Report](#before-submitting-a-bug-report) + - [How Do I Submit A (Good) Bug Report?](#how-do-i-submit-a-good-bug-report) +- [Suggesting Enhancements](#suggesting-enhancements) + - [Before Submitting An Enhancement Suggestion](#before-submitting-an-enhancement-suggestion) + - [How Do I Submit A (Good) Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) +- [Your First Code Contribution](#your-first-code-contribution) +- [Pull Requests](#pull-requests) + +## Reporting Bugs + +This section guides you through submitting a bug report. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. + +Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template][bugreport], the information it asks for helps us resolve issues faster. + +> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. + +### Before Submitting A Bug Report + +* **Perform a cursory search** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. + +### How Do I Submit A (Good) Bug Report? + +Bugs are tracked as [GitHub issues][githubissues]. Create an issue and provide the following information by filling in [the template][bugreport]. + +Explain the problem and include additional details to help maintainers reproduce the problem: + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started. When listing steps, **don't just say what you did, but explain how you did it**. +* **Provide specific examples to demonstrate the steps**. Include links to files or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks][githubcodeblocks]. +* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. +* **Explain which behavior you expected to see instead and why.** +* **Include screenshots and animated GIFs** where possible. Show how you follow the described steps and clearly demonstrate the problem. You can use [this tool][licecap] to record GIFs on macOS and Windows, and [this tool][silentcast] on Linux. +* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. +* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. +Include details about your configuration and environment: + +## Suggesting Enhancements + +This section guides you through submitting a suggestion, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. + +Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill out [the required template][featurerequest], the information it asks for helps us resolve issues faster. + +### Before Submitting An Enhancement Suggestion + +* **Perform a cursory search** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. + +### How Do I Submit A (Good) Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues][githubissues]. Create an issue and provide the following information by filling in [the template][featurerequest]. + +* **Use a clear and descriptive title** for the issue to identify the suggestion. +* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. +* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks][githubcodeblocks]. +* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. +* **Explain why this enhancement would be useful** to most users. + +## Pull Requests + +Please follow these steps to have your contribution considered by the maintainers: + +1. Follow all instructions in [the template][pullrequest] +2. Adhear the [Code of Conduct][coc] +3. After you submit your pull request, verify that all [status checks][githubstatuschecks] are passing. + +While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. + +# I don't want to read this whole thing I just have a question! + +You can join the Vonage Community Slack for any questions you might have: + +* [Contact our Developer Relations Team][community] +* [Reach out on Twitter][twitter] + * This Twitter is monitored by our Developer Relations team, but not 24/7 — please be patient! +* [Join the Vonage Community Slack][slack] + * Even though Slack is a chat service, sometimes it takes several hours for community members to respond — please be patient! + * Use the `#general` channel for general questions or discussion + * Use the `#status` channel for receiving updates on our service status + * There are many other channels available, check the channel list for channels for a specific library + +Alternatively, you can raise an issue on the project. + +[beginner]:https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Anexmo-community+sort%3Acomments-desc +[help-wanted]:https://github.com/search?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Anexmo-community+sort%3Acomments-desc+-label%3Abeginner + +[contribadge]: https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat "Contributions Welcome" + +[coc]: CODE_OF_CONDUCT.md "Code of Conduct" +[contributing]: CONTRIBUTING.md "Contributing" +[license]: LICENSE "MIT License" +[pullrequest]: PULL_REQUEST_TEMPLATE.md "Pull Request template" + +[community]: https://developer.nexmo.com/community "Vonage Community" +[signup]: https://dashboard.nexmo.com/sign-up?utm_source=DEV_REL&utm_medium=github&utm_campaign=phzn +[slack]: https://developer.nexmo.com/community/slack "Vonage Community Slack" +[twitter]: https://twitter.com/VonageDev "VonageDev on Twitter" + +[bugreport]: ./../../issues/new?assignees=&labels=&template=bug_report.md&title= "Bug Report Template" +[featurerequest]: ./../../issues/new?assignees=&labels=&template=feature_request.md&title= "Enhancement Suggestion Template" +[issues]: ./../../issues "Issues" +[pulls]: ./../../pulls "Pull requests" + +[githubcodeblocks]: https://help.github.com/articles/markdown-basics/#multiple-lines "GitHub Markdown Code Blocks" +[githubflow]: https://guides.github.com/introduction/flow/index.html "GitHub Flow" +[githubstatuschecks]: https://help.github.com/articles/about-status-checks/ "GitHub Status Checks" +[licecap]: https://www.cockos.com/licecap/ "LICEcap: animated screen captures" +[silentcast]: https://github.com/colinkeenan/silentcast "Silentcast: silent mkv screencasts and animated gifs" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6772d7a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Chen Hui Jing + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d38044a --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ +* **Please check if the PR fulfills these requirements** +- [ ] You've followed the [contributing guidelines][contributing] +- [ ] You've adheared the [code of conduct][coc] + +* **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) + +- [ ] Bug Fix +- [ ] New Feature +- [ ] Documentation Update +- [ ] Other + +## If bug fixes or new features + +* **What is the current behavior?** (You can also link to an open issue here) + + + +* **What is the new behavior (if this is a feature change)?** + + + +* **Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) + + + +* **Other information**: + +[coc]: CODE_OF_CONDUCT.md "Code of Conduct" +[contributing]: CONTRIBUTING.md "Contributing" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3e2f6d --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Broadcast a Basic Video Chat + +This series of tutorials will explore the [Vonage Video API (formerly TokBox OpenTok)](https://tokbox.com/developer/) and what you can build with it. The Video API is very robust and highly customizable, and in each post, we’ll show how to implement a specific feature using the API. This time we will look at how to broadcast your video chat to many viewers online. + +We will not be using any front-end frameworks for this series, just vanilla Javascript to keep the focus on the Video API itself. At the end of this tutorial, you should be able to broadcast your video chat live to a large audience using HTTP live streaming (HLS) or an RTMP stream. + +![Screenshot of broadcast page](https://cdn.glitch.com/ca52d415-205d-46a8-a682-76597d9011f8%2Fbroadcast.jpg?v=1587378393372) + +## Running on your local machine + +1. `git clone https://github.com/nexmo-community/broadcast-video-chat.git` +2. `npm install` +3. `node server.js` + +## Code of Conduct + +In the interest of fostering an open and welcoming environment, we strive to make participation in our project and our community a harassment-free experience for everyone. Please check out our [Code of Conduct][coc] in full. + +## Contributing + +We :heart: contributions from everyone! Check out the [Contributing Guidelines][contributing] for more information. + +[![contributions welcome][contribadge]][issues] + +## License + +This project is subject to the [MIT License][license] + +[contribadge]: https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat "Contributions Welcome" + +[coc]: CODE_OF_CONDUCT.md "Code of Conduct" +[contributing]: CONTRIBUTING.md "Contributing" +[license]: LICENSE "MIT License" + +[issues]: ./../../issues "Issues" diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f0df88 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "broadcast-video-chat", + "version": "0.0.1", + "description": "Broadcast your video chat to a large audience with Vonage Video API in Node.js", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.17.1", + "opentok": "^2.9.2" + }, + "engines": { + "node": "12.x" + }, + "repository": { + "url": "https://glitch.com/edit/#!/basic-video-chat" + }, + "license": "MIT", + "keywords": [ + "node", + "glitch", + "express" + ], + "devDependencies": { + "dotenv": "^8.2.0" + } +} diff --git a/public/broadcast.js b/public/broadcast.js new file mode 100644 index 0000000..b2342c6 --- /dev/null +++ b/public/broadcast.js @@ -0,0 +1,31 @@ +const url = new URL(window.location.href); +const roomName = url.pathname.split("/")[2]; +const hlsLink = url.searchParams.get("hls"); + +fetch("/broadcast/hls/" + roomName) + .then(res => { + return res.json(); + }) + .then(res => { + playStream(hlsLink); + }) + .catch(error => console.error(error)); + +function playStream(hlsLink) { + const video = document.getElementById("video"); + const videoSrc = hlsLink; + + if (Hls.isSupported()) { + const hls = new Hls(); + hls.loadSource(videoSrc); + hls.attachMedia(video); + hls.on(Hls.Events.MANIFEST_PARSED, function() { + video.play(); + }); + } else if (video.canPlayType("application/vnd.apple.mpegurl")) { + video.src = videoSrc; + video.addEventListener("loadedmetadata", function() { + video.play(); + }); + } +} diff --git a/public/client.js b/public/client.js new file mode 100644 index 0000000..7e8d268 --- /dev/null +++ b/public/client.js @@ -0,0 +1,173 @@ +let session; +let broadcast; +const url = new URL(window.location.href); +const roomName = url.pathname.split("/")[2]; + +fetch(location.pathname, { method: "POST" }) + .then(res => { + return res.json(); + }) + .then(res => { + const apiKey = res.apiKey; + const sessionId = res.sessionId; + const token = res.token; + initializeSession(apiKey, sessionId, token); + }) + .catch(handleCallback); + +registerListeners(); + +function initializeSession(apiKey, sessionId, token) { + // Create a session object with the sessionId + session = OT.initSession(apiKey, sessionId); + + // Create a publisher + const publisher = OT.initPublisher( + "publisher", + { + insertMode: "append", + width: "100%", + height: "100%" + }, + handleCallback + ); + + // Connect to the session + session.connect(token, error => { + // If the connection is successful, initialize the publisher and publish to the session + if (error) { + handleCallback(error); + } else { + session.publish(publisher, handleCallback); + } + }); + + // Subscribe to a newly created stream + session.on("streamCreated", event => { + session.subscribe( + event.stream, + "subscriber", + { + insertMode: "append", + width: "100%", + height: "100%" + }, + handleCallback + ); + }); +} + +// Callback handler +function handleCallback(error) { + if (error) { + console.log("error: " + error.message); + } else { + console.log("callback success"); + } +} + +function registerListeners() { + const startBroadcastBtn = document.getElementById("startBroadcast"); + const stopBroadcastBtn = document.getElementById("stopBroadcast"); + const copyLinkBtn = document.getElementById("copyLink"); + + startBroadcastBtn.addEventListener("click", startBroadCast, false); + stopBroadcastBtn.addEventListener("click", stopBroadCast, false); + copyLinkBtn.addEventListener("click", copyHlsLink, false); +} + +function startBroadCast() { + pendingBtnState("start"); + + fetch("/broadcast/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: session.sessionId }) + }) + .then(res => { + return res.json(); + }) + .then(res => { + broadcast = res.broadcast; + activeBtnState("stop"); + composeHlsLink(res.broadcast.broadcastUrls.hls); + }) + .catch(handleCallback); +} + +function stopBroadCast() { + pendingBtnState("stop"); + + fetch("/broadcast/stop", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ broadcastId: broadcast.id }) + }) + .then(res => { + return res.json(); + }) + .then(res => { + activeBtnState("start"); + hlsLinkState("stop"); + }) + .catch(handleCallback); +} + +function pendingBtnState(statusString) { + const btn = document.getElementById(statusString + "Broadcast"); + btn.classList.add("disabled"); + btn.setAttribute("data-original", btn.textContent); + btn.textContent = "Processing…"; +} + +function activeBtnState(statusString) { + const activeBtn = + statusString === "start" + ? document.getElementById("startBroadcast") + : document.getElementById("stopBroadcast"); + const inactiveBtn = + statusString === "stop" + ? document.getElementById("startBroadcast") + : document.getElementById("stopBroadcast"); + + inactiveBtn.classList.remove("disabled"); + inactiveBtn.textContent = inactiveBtn.getAttribute("data-original"); + inactiveBtn.removeAttribute("data-original"); + inactiveBtn.classList.add("hidden"); + activeBtn.classList.remove("hidden"); +} + +function hlsLinkState(statusString) { + if (statusString === "start") { + document.getElementById("hlsLink").classList.remove("hidden"); + document.getElementById("copyLink").classList.remove("hidden"); + } else { + document.getElementById("hlsLink").classList.add("hidden"); + document.getElementById("copyLink").classList.add("hidden"); + } +} + +function composeHlsLink(link) { + hlsLinkState("start"); + const hlsLinkUrl = location.host + "/broadcast/" + roomName + "?hls=" + link; + const hlsLink = document.getElementById("hlsLink"); + const hlsCopyTarget = document.getElementById("hlsCopyTarget"); + hlsLink.href = hlsLinkUrl; + hlsCopyTarget.innerHTML = hlsLinkUrl; +} + +function copyHlsLink() { + const hlsCopyTarget = document.getElementById("hlsCopyTarget"); + const range = document.createRange(); + range.selectNode(hlsCopyTarget); + window.getSelection().addRange(range); + + try { + const successful = document.execCommand("copy"); + const msg = successful ? "successful" : "unsuccessful"; + console.log("Copy command was " + msg); + } catch (err) { + console.log("Oops, unable to copy"); + } + window.getSelection().removeAllRanges(); +} diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..b711d91 --- /dev/null +++ b/public/style.css @@ -0,0 +1,100 @@ +html { + box-sizing: border-box; + height: 100%; +} + +*, +*::before, +*::after { + box-sizing: inherit; + margin: 0; + padding: 0; +} + +body { + height: 100%; + display: flex; + flex-direction: column; +} + +header { + text-align: center; + padding: 0.5em; +} + +main { + flex: 1; + display: flex; + position: relative; +} + +input, +button { + font-size: inherit; + padding: 0.5em; +} + +.registration { + display: flex; + flex-direction: column; + margin: auto; +} + +.registration input[type="text"] { + display: block; + margin-bottom: 1em; +} + +.subscriber { + width: 100%; + height: 100%; + display: flex; +} + +.publisher { + position: absolute; + width: 25vmin; + height: 25vmin; + min-width: 8em; + min-height: 8em; + align-self: flex-end; +} + +.broadcast { + position: absolute; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.broadcast a, +.broadcast button { + margin-bottom: 1em; +} + +.hidden { + display: none; +} + +.invisible { + position: absolute; + opacity: 0; + z-index: -1; +} + +.disabled { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; +} + +.broadcast-video { + margin: auto; +} + +footer { + text-align: center; + padding: 0.5em; +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..f1afbb4 --- /dev/null +++ b/server.js @@ -0,0 +1,113 @@ +require('dotenv').config(); + +const express = require("express"); +const app = express(); +const OpenTok = require("opentok"); +const OT = new OpenTok(process.env.API_KEY, process.env.API_SECRET); + +let sessions = {}; +let broadcastData = {}; + +app.use(express.static("public")); +app.use(express.json()); + +app.get("/", (request, response) => { + response.sendFile(__dirname + "/views/landing.html"); +}); + +app.get("/session/:room", (request, response) => { + response.sendFile(__dirname + "/views/index.html"); +}); + +app.get("/broadcast/:room", (request, response) => { + response.sendFile(__dirname + "/views/broadcast.html"); +}); + +app.get("/broadcast/hls/:room", (request, response) => { + const roomName = request.params.room; + if (sessions[roomName]) { + response.status(200); + response.send({ + hls: broadcastData.broadcastUrls.hls, + status: broadcastData.status + }); + } else { + response.status(204); + } +}); + +app.post("/session/:room", (request, response) => { + const roomName = request.params.room; + // Check if the session already exists + if (sessions[roomName]) { + // Generate the token + generateToken(roomName, response); + } else { + // If the session does not exist, create one + OT.createSession({ mediaMode: "routed" }, (error, session) => { + if (error) { + console.log("Error creating session:", error); + } else { + // Store the session in the sessions object + sessions[roomName] = session.sessionId; + // Generate the token + generateToken(roomName, response); + } + }); + } +}); + +app.post("/broadcast/start", (request, response) => { + const sessionId = request.body.sessionId; + console.log(sessionId); + + const broadcastOptions = { + outputs: { + hls: {}, + }, + }; + + OT.startBroadcast(sessionId, broadcastOptions, (error, broadcast) => { + if (error) { + console.log(error); + response.status(503); + response.send({ error }); + } + broadcastData = broadcast; + response.status(200); + response.send({ broadcast: broadcast }); + }); +}); + +app.post("/broadcast/stop", (request, response) => { + const broadcastId = request.body.broadcastId; + OT.stopBroadcast(broadcastId, (error, broadcast) => { + if (error) console.log(error); + response.status(200); + response.send({ + status: broadcast.status + }); + }); +}); + +function generateToken(roomName, response) { + // Configure token options + const tokenOptions = { + role: "publisher", + data: `roomname=${roomName}` + }; + // Generate token with the Video API Client SDK + let token = OT.generateToken(sessions[roomName], tokenOptions); + // Send the required credentials back to to the client + // as a response from the fetch request + response.status(200); + response.send({ + sessionId: sessions[roomName], + token: token, + apiKey: process.env.API_KEY + }); +} + +const listener = app.listen(process.env.PORT, () => { + console.log("Your app is listening on port " + listener.address().port); +}); \ No newline at end of file diff --git a/views/broadcast.html b/views/broadcast.html new file mode 100644 index 0000000..63539fd --- /dev/null +++ b/views/broadcast.html @@ -0,0 +1,43 @@ + + + + Broadcast Video Chat + + + + + + + + + + +
+

Video broadcast

+
+ +
+ +
+ + + + + + + \ No newline at end of file diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..4a0dc24 --- /dev/null +++ b/views/index.html @@ -0,0 +1,54 @@ + + + + Broadcast Video Chat + + + + + + + + + + +
+

Broadcast your video chat

+
+ +
+
+
+ +
+ + + + + +
+
+ + + + + + + diff --git a/views/landing.html b/views/landing.html new file mode 100644 index 0000000..51ab1ee --- /dev/null +++ b/views/landing.html @@ -0,0 +1,59 @@ + + + + Broadcast Video Chat + + + + + + + + + + +
+

Broadcast your video chat

+
+ +
+
+ + +
+
+ + + + + +