├── src ├── App.css ├── assets │ └── constants.js ├── styles │ ├── videoPlayerStyles.js │ ├── headerStyles.js │ ├── appStyles.js │ ├── globalStyles.js │ ├── liveChatStyles.js │ └── videoDetailsStyles.js ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── components │ ├── Header.jsx │ ├── LiveChat.jsx │ ├── VideoPlayer.jsx │ └── VideoDetails.jsx ├── context │ ├── RTCPeerContext.jsx │ └── SocketContext.jsx ├── index.js ├── logo.svg └── App.js ├── .gitignore ├── public ├── config.json ├── .DS_Store ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── images │ ├── .DS_Store │ ├── videoPoster.jpg │ └── lightspeedlogo.svg ├── manifest.json └── index.html ├── .dockerignore ├── docker ├── config.json.template └── entrypoint.sh ├── .eslintrc.json ├── Dockerfile ├── LICENSE ├── package.json ├── images └── lightspeedlogo.svg └── README.md /src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .idea/ 3 | .DS_Store 4 | .eslintcache 5 | /build/ -------------------------------------------------------------------------------- /public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "wsUrl": "ws://stream.gud.software:8080/websocket" 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dockerignore 3 | .gitignore 4 | Dockerfile 5 | README.md 6 | LICENSE -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Lightspeed-react/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Lightspeed-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Lightspeed-react/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Lightspeed-react/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docker/config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "wsUrl": "ws://${WEBSOCKET_HOST}:${WEBSOCKET_PORT}/websocket" 3 | } 4 | -------------------------------------------------------------------------------- /public/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Lightspeed-react/HEAD/public/images/.DS_Store -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | envsubst < /config.json.template > "/usr/share/nginx/html/config.json" 4 | -------------------------------------------------------------------------------- /public/images/videoPoster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Lightspeed-react/HEAD/public/images/videoPoster.jpg -------------------------------------------------------------------------------- /src/assets/constants.js: -------------------------------------------------------------------------------- 1 | export const LightspeedLogoURL = "/images/lightspeedlogo.svg"; 2 | export const VideoPosterURL = "/images/videoPoster.jpg"; 3 | -------------------------------------------------------------------------------- /src/styles/videoPlayerStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Video = styled.video` 4 | border-radius: 0px; 5 | z-index: 10; 6 | width: 100%; 7 | `; 8 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | //"extends": ["eslint:recommended", "plugin:react/recommended"], 7 | "extends": ["plugin:react/recommended"], 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["react"], 16 | "rules": { 17 | "react/jsx-uses-react": "error", 18 | "react/jsx-uses-vars": "error" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HeaderLogoContainer, MainHeader } from "../styles/headerStyles"; 3 | import { LightspeedLogoURL } from "../assets/constants"; 4 | 5 | const Header = () => { 6 | return ( 7 | 8 | 9 | Lightspeed logo 10 |

Project Lightspeed

11 |
12 |
13 | ); 14 | }; 15 | 16 | export default Header; 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ALPINE_VERSION=3.12 2 | ARG NODE_VERSION=15 3 | ARG NGINX_VERSION=1.19.6 4 | 5 | FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS builder 6 | WORKDIR /app/Lightspeed-react 7 | COPY package.json package-lock.json ./ 8 | RUN npm install 9 | COPY . . 10 | RUN npm run build 11 | 12 | 13 | FROM nginx:${NGINX_VERSION}-alpine 14 | ENV WEBSOCKET_HOST=localhost 15 | ENV WEBSOCKET_PORT=8080 16 | EXPOSE 80/tcp 17 | COPY --chown=1000 docker/entrypoint.sh /docker-entrypoint.d/entrypoint.sh 18 | COPY --chown=1000 docker/config.json.template /config.json.template 19 | COPY --from=builder --chown=1000 /app/Lightspeed-react/build /usr/share/nginx/html 20 | -------------------------------------------------------------------------------- /src/styles/headerStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const MainHeader = styled.header` 4 | background: #1f2128; 5 | display: flex; 6 | flex-direction: row; 7 | align-items: center; 8 | justify-content: center; 9 | border: 0.5px solid rgba(240, 243, 246, 0.1); 10 | padding: 1em; 11 | `; 12 | 13 | export const HeaderLogoContainer = styled.div` 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | 18 | h1 { 19 | font-weight: 600; 20 | font-size: 2em; 21 | color: white; 22 | } 23 | 24 | img { 25 | height: 90px; 26 | margin: auto; 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/styles/appStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const MainContainer = styled.div` 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-evenly; 7 | margin: 2em; 8 | 9 | @media only screen and (max-width: 1024px) { 10 | margin: 1.5em 0; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-evenly; 14 | } 15 | `; 16 | 17 | export const VideoContainer = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | color: #fff; 21 | margin: 0 2.5em; 22 | 23 | @media only screen and (max-width: 1024px) { 24 | margin: 0.3em; 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/components/LiveChat.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | ChatContainer, 4 | ChatMain, 5 | ChatHeading, 6 | ChatBody, 7 | } from "../styles/liveChatStyles"; 8 | 9 | const LiveChat = () => { 10 | return ( 11 | 12 | 13 | 14 |
Live Chat Room
15 | 16 |
17 | 18 | 19 | 20 |

Coming Soon!

21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default LiveChat; 28 | -------------------------------------------------------------------------------- /src/styles/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | *{ 5 | padding: 0; 6 | margin:0; 7 | border: 0; 8 | font-family: 'Poppins', sans-serif; 9 | font-style: normal; 10 | text-align:center; 11 | } 12 | 13 | body{ 14 | background-color: #1f2128; 15 | } 16 | 17 | .App{ 18 | text-align:center; 19 | } 20 | 21 | h4 { 22 | font-weight: 500; 23 | font-size: 32px; 24 | line-height: 48px; 25 | letter-spacing: -0.5px; 26 | } 27 | 28 | h6 { 29 | font-weight: 500; 30 | font-size: 18px; 31 | line-height: 24px; 32 | } 33 | `; 34 | 35 | export default GlobalStyle; 36 | -------------------------------------------------------------------------------- /src/components/VideoPlayer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { VideoPosterURL } from "../assets/constants"; 4 | import { Video } from "../styles/videoPlayerStyles"; 5 | 6 | const VideoPlayer = ({ src }) => { 7 | const videoRef = useRef(null); 8 | 9 | useEffect(() => { 10 | if (src) { 11 | videoRef.current.srcObject = src; 12 | } 13 | }, [src]); 14 | 15 | return ( 16 | 24 | ); 25 | }; 26 | 27 | export default VideoPlayer; 28 | 29 | VideoPlayer.propTypes = { 30 | src: PropTypes.object, 31 | }; 32 | -------------------------------------------------------------------------------- /src/context/RTCPeerContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export const RTCContext = createContext(); 5 | 6 | const RTCProvider = ({ children }) => { 7 | const [pc] = useState(new RTCPeerConnection()); 8 | 9 | const value = { 10 | pc, 11 | }; 12 | 13 | return {children}; 14 | }; 15 | 16 | export const useRTC = () => { 17 | const context = useContext(RTCContext); 18 | 19 | if (!context) { 20 | throw new Error("useRTC must be nested in RTCProvider"); 21 | } 22 | 23 | return context; 24 | }; 25 | 26 | export default RTCProvider; 27 | 28 | RTCProvider.propTypes = { 29 | children: PropTypes.object, 30 | }; 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import SocketProvider from "./context/SocketContext"; 7 | import RTCProvider from "./context/RTCPeerContext"; 8 | import GlobalStyle from "./styles/globalStyles"; 9 | 10 | ReactDOM.render( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById("root") 20 | ); 21 | 22 | // If you want to start measuring performance in your app, pass a function 23 | // to log results (for example: reportWebVitals(console.log)) 24 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 25 | reportWebVitals(); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Garrett GRVY Graves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lightspeed-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.6", 7 | "@testing-library/react": "^11.2.2", 8 | "@testing-library/user-event": "^12.6.0", 9 | "plyr": "^3.6.3", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-scripts": "4.0.1", 13 | "styled-components": "^5.2.1", 14 | "web-vitals": "^0.2.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "eslint": "^7.17.0", 42 | "eslint-plugin-react": "^7.22.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/VideoDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | DetailHeadingBox, 5 | VideoDetailsContainer, 6 | DetailsTitle, 7 | DetailsHeading, 8 | DetailsTop, 9 | AlphaTag, 10 | ViewerTag, 11 | } from "../styles/videoDetailsStyles"; 12 | import { LightspeedLogoURL } from "../assets/constants"; 13 | 14 | const VideoDetails = ({ viewers }) => { 15 | return ( 16 | 17 | 18 | 19 | 20 | Alpha 21 | 22 | 23 | 24 | {viewers} 25 | 26 | 27 | 28 | 29 | Welcome to Project Lightspeed 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default VideoDetails; 38 | 39 | VideoDetails.propTypes = { 40 | viewers: PropTypes.number, 41 | }; 42 | -------------------------------------------------------------------------------- /images/lightspeedlogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/lightspeedlogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/liveChatStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ChatContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | position: relative; 7 | color: #fff; 8 | margin: 0 2.5em; 9 | min-width: 25em; 10 | 11 | @media only screen and (max-width: 1024px) { 12 | margin: 1em 0.3em; 13 | min-width: unset; 14 | } 15 | `; 16 | 17 | export const ChatMain = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | height: 100%; 21 | width: 100%; 22 | background: #242731; 23 | border: 0.5px solid rgba(240, 243, 246, 0.2); 24 | border-radius: 32px; 25 | `; 26 | 27 | export const ChatHeading = styled.div` 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | align-items: center; 32 | padding: 0 2rem; 33 | 34 | h6 { 35 | margin: 1em 0; 36 | } 37 | 38 | .arrow { 39 | margin-top: auto; 40 | margin-bottom: auto; 41 | transform: rotate(45deg); 42 | } 43 | `; 44 | 45 | export const ChatBody = styled.div` 46 | display: flex; 47 | flex-direction: column; 48 | width: 100%; 49 | height: 100%; 50 | justify-content: center; 51 | border-top: 0.5px solid rgba(240, 243, 246, 0.1); 52 | border-radius: 32px; 53 | 54 | i { 55 | font-weight: 900px; 56 | } 57 | 58 | @media only screen and (max-width: 1024px) { 59 | min-height: 300px; 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /src/styles/videoDetailsStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const VideoDetailsContainer = styled.div ` 4 | width: 100%; 5 | background-color: #242731; 6 | text-align: left; 7 | padding-top: 4em; 8 | margin-top: -3em; 9 | border-radius: 32px; 10 | `; 11 | 12 | export const DetailHeadingBox = styled.div ` 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | margin: 0 2em 3em 2em; 17 | 18 | img { 19 | height: 130px; 20 | width: 130px; 21 | 22 | 23 | @media only screen and (max-width: 1024px) { 24 | display: none; 25 | } 26 | } 27 | `; 28 | 29 | export const DetailsTitle = styled.div ` 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | `; 34 | 35 | export const DetailsTop = styled.div ` 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: space-between; 39 | margin-bottom: 1rem; 40 | padding-left: 2rem; 41 | `; 42 | 43 | export const DetailsHeading = styled.h4 ` 44 | font-size: 30px; 45 | `; 46 | export const ViewerTag = styled.div ` 47 | display: flex; 48 | flex-direction: row; 49 | justify-content: space-evenly; 50 | height: 35px; 51 | width: 110px; 52 | 53 | 54 | border-radius: 8px; 55 | 56 | i { 57 | margin: auto 0; 58 | } 59 | 60 | span { 61 | margin: auto 0; 62 | font-weight: 600; 63 | } 64 | `; 65 | export const AlphaTag = styled.div ` 66 | display: flex; 67 | flex-direction: row; 68 | justify-content: space-evenly; 69 | height: 35px; 70 | width: 110px; 71 | text-align: center; 72 | background-color: #ff754c; 73 | border-radius: 8px; 74 | 75 | i { 76 | margin: auto 0; 77 | } 78 | 79 | span { 80 | margin: auto 0; 81 | } 82 | `; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 28 | Lightspeed 29 | 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import React, { useEffect, useReducer } from "react"; 3 | import { useSocket } from "./context/SocketContext"; 4 | import { useRTC } from "./context/RTCPeerContext"; 5 | import VideoPlayer from "./components/VideoPlayer"; 6 | import VideoDetails from "./components/VideoDetails"; 7 | import LiveChat from "./components/LiveChat"; 8 | import Header from "./components/Header"; 9 | import { VideoContainer, MainContainer } from "./styles/appStyles"; 10 | 11 | const appReducer = (state, action) => { 12 | switch (action.type) { 13 | case "initStream": { 14 | return { ...state, stream: action.stream }; 15 | } 16 | case "info": { 17 | return { ...state, viewers: action.viewers }; 18 | } 19 | 20 | default: { 21 | return { ...state }; 22 | } 23 | } 24 | }; 25 | 26 | const initialState = { 27 | stream: null, 28 | viewers: null, 29 | }; 30 | 31 | const App = () => { 32 | const [state, dispatch] = useReducer(appReducer, initialState); 33 | const { pc } = useRTC(); 34 | const { socket } = useSocket(); 35 | 36 | // Offer to receive 1 audio, and 1 video tracks 37 | pc.addTransceiver("audio", { direction: "recvonly" }); 38 | // pc.addTransceiver('video', { 'direction': 'recvonly' }) 39 | pc.addTransceiver("video", { direction: "recvonly" }); 40 | 41 | pc.ontrack = (event) => { 42 | const { 43 | track: { kind }, 44 | streams, 45 | } = event; 46 | 47 | if (kind === "video") { 48 | dispatch({ type: "initStream", stream: streams[0] }); 49 | } 50 | }; 51 | 52 | pc.onicecandidate = (e) => { 53 | const { candidate } = e; 54 | if (candidate) { 55 | console.log("Candidate success"); 56 | socket.send( 57 | JSON.stringify({ 58 | event: "candidate", 59 | data: e.candidate, 60 | }) 61 | ); 62 | } 63 | }; 64 | 65 | if (socket) { 66 | socket.onmessage = async (event) => { 67 | const msg = JSON.parse(event.data); 68 | 69 | if (!msg) { 70 | console.log("Failed to parse msg"); 71 | return; 72 | } 73 | 74 | const offerCandidate = msg.data; 75 | 76 | if (!offerCandidate) { 77 | console.log("Failed to parse offer msg data"); 78 | return; 79 | } 80 | 81 | switch (msg.event) { 82 | case "offer": 83 | console.log("Offer"); 84 | pc.setRemoteDescription(offerCandidate); 85 | 86 | try { 87 | const answer = await pc.createAnswer(); 88 | pc.setLocalDescription(answer); 89 | socket.send( 90 | JSON.stringify({ 91 | event: "answer", 92 | data: answer, 93 | }) 94 | ); 95 | } catch (e) { 96 | console.error(e.message); 97 | } 98 | 99 | return; 100 | case "candidate": 101 | console.log("Candidate"); 102 | pc.addIceCandidate(offerCandidate); 103 | return; 104 | case "info": 105 | dispatch({ 106 | type: "info", 107 | viewers: msg.data.no_connections, 108 | }); 109 | } 110 | }; 111 | } 112 | 113 | return ( 114 | <> 115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | ); 125 | }; 126 | 127 | export default App; 128 | -------------------------------------------------------------------------------- /src/context/SocketContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useReducer } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export const SocketContext = createContext(); 5 | 6 | const socketReducer = (state, action) => { 7 | switch (action.type) { 8 | case "initSocket": { 9 | return { 10 | ...state, 11 | socket: new WebSocket(action.url), 12 | url: action.url, 13 | }; 14 | } 15 | case "renewSocket": { 16 | let timeout = state.wsTimeoutDuration * 2; 17 | if (timeout > 10000) { 18 | timeout = 10000; 19 | } 20 | return { 21 | ...state, 22 | socket: new WebSocket(state.url), 23 | wsTimeoutDuration: timeout, 24 | }; 25 | } 26 | case "updateTimeout": { 27 | return { ...state, connectTimeout: action.timeout }; 28 | } 29 | case "clearTimeout": { 30 | clearTimeout(state.connectTimeout); 31 | return { ...state }; 32 | } 33 | case "resetTimeoutDuration": { 34 | return { ...state, wsTimeoutDuration: 250 }; 35 | } 36 | default: { 37 | return { ...state }; 38 | } 39 | } 40 | }; 41 | 42 | const initialState = { 43 | url: "", 44 | socket: null, 45 | wsTimeoutDuration: 250, 46 | connectTimeout: null, 47 | }; 48 | 49 | const SocketProvider = ({ children }) => { 50 | const [state, dispatch] = useReducer(socketReducer, initialState); 51 | 52 | const { socket, wsTimeoutDuration } = state; 53 | 54 | useEffect(() => { 55 | // run once on first render 56 | (async () => { 57 | try { 58 | const response = await fetch("config.json"); 59 | const data = await response.json(); 60 | if (Object.prototype.hasOwnProperty.call(data, "wsUrl")) { 61 | dispatch({ 62 | type: "initSocket", 63 | url: data.wsUrl, 64 | }); 65 | } else { 66 | console.error("config.json is invalid"); 67 | } 68 | } catch (e) { 69 | console.error(e.message); 70 | } 71 | })(); 72 | }, []); 73 | 74 | useEffect(() => { 75 | if (!socket) return; 76 | 77 | socket.onopen = () => { 78 | dispatch({ type: "resetTimeout" }); 79 | dispatch({ type: "resetTimeoutDuration" }); 80 | console.log("Connected to websocket"); 81 | }; 82 | 83 | socket.onclose = (e) => { 84 | const { reason } = e; 85 | console.log( 86 | `Socket is closed. Reconnect will be attempted in ${Math.min( 87 | wsTimeoutDuration / 1000 88 | )} second. ${reason}` 89 | ); 90 | 91 | const timeout = setTimeout(() => { 92 | //check if websocket instance is closed, if so renew connection 93 | if (!socket || socket.readyState === WebSocket.CLOSED) { 94 | dispatch({ type: "renewSocket" }); 95 | } 96 | }, wsTimeoutDuration); 97 | 98 | dispatch({ 99 | type: "updateTimeout", 100 | timeout, 101 | }); 102 | }; 103 | 104 | // err argument does not have any useful information about the error 105 | socket.onerror = () => { 106 | console.error(`Socket encountered error. Closing socket.`); 107 | socket.close(); 108 | }; 109 | }, [socket]); 110 | 111 | const value = { 112 | socket: state.socket, 113 | }; 114 | 115 | return ( 116 | {children} 117 | ); 118 | }; 119 | 120 | export const useSocket = () => { 121 | const context = useContext(SocketContext); 122 | 123 | if (!context) { 124 | throw new Error("useSocket must be nested in SocketProvider"); 125 | } 126 | 127 | return context; 128 | }; 129 | 130 | export default SocketProvider; 131 | 132 | SocketProvider.propTypes = { 133 | children: PropTypes.object, 134 | }; 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 |

6 |

Project Lightspeed React [Deprecated]

7 |
8 | Stars Badge 9 | Forks Badge 10 | Pull Requests Badge 11 | Issues Badge 12 | GitHub contributors 13 | License Badge 14 |
15 |
16 |

17 |

18 | NOTE: This repo has been deprecated in favor of a monorepo configuration. Please see
19 | A React website that connects to Lightspeed WebRTC via a websocket to negotiate SDPs and display a WebRTC stream. 20 | 21 | 22 |
23 |
24 | View Demo 25 | · 26 | Report Bug 27 | · 28 | Request Feature 29 |

30 |

31 | 32 | 33 |
34 |

Table of Contents

35 |
    36 |
  1. 37 | About The Project 38 | 41 |
  2. 42 |
  3. 43 | Getting Started 44 | 48 |
  4. 49 |
  5. Usage
  6. 50 |
  7. Roadmap
  8. 51 |
  9. Contributing
  10. 52 |
  11. License
  12. 53 |
  13. Contact
  14. 54 |
  15. Acknowledgements
  16. 55 |
56 |
57 | 58 | 59 | 60 | ## About The Project 61 | 62 | 63 | 64 | This is one of three components required for Project Lightspeed. Project Lightspeed is a fully self contained live streaming server. With this you will be able to deploy your own sub-second latency live streaming platform. This particular repository connects via websocket to Lightspeed WebRTC and displays a WebRTC stream. In order for this to work the Project Lightspeed WebRTC and Project Lightspeed Ingest are required. 65 | 66 | ### Built With 67 | 68 | - React 69 | 70 | ### Dependencies 71 | 72 | - [Lightspeed WebRTC](https://github.com/GRVYDEV/Lightspeed-webrtc) 73 | - [Lightspeed Ingest](https://github.com/GRVYDEV/Lightspeed-ingest) 74 | 75 | 76 | 77 | ## Getting Started 78 | 79 | ## Setup 80 | 81 | ### Docker 82 | 83 | 1. Install [git](https://git-scm.com/downloads) 84 | 1. Build the image from the master branch with: 85 | 86 | ```sh 87 | docker build -t grvydev/lightspeed-react https://github.com/GRVYDEV/Lightspeed-react.git 88 | ``` 89 | 90 | 1. Run it with 91 | 92 | ```sh 93 | docker run -it --rm \ 94 | -p 8000:80/tcp \ 95 | -e WEBSOCKET_HOST=localhost \ 96 | -e WEBSOCKET_PORT=8080 \ 97 | grvydev/lightspeed-react 98 | ``` 99 | 100 | Where your websocket host from the browser/client perspective is accessible on `localhost:8080`. 101 | 102 | 1. You can now access it at [localhost:8000](http://localhost:8000). 103 | 104 | ### Locally 105 | 106 | To get a local copy up and running follow these simple steps. 107 | 108 | #### Prerequisites 109 | 110 | In order to run this npm is required. Installation instructions can be found here. Npm Serve is required as well if you want to host this on your machine. That can be found here 111 | 112 | #### Installation 113 | 114 | ```sh 115 | git clone https://github.com/GRVYDEV/Lightspeed-react.git 116 | cd Lightspeed-react 117 | npm install 118 | ``` 119 | 120 | 121 | 122 | #### Usage 123 | 124 | First build the frontend 125 | 126 | ```sh 127 | cd Lightspeed-react 128 | npm run build 129 | ``` 130 | 131 | You should then configure the websocket URL in `config.json` in the `build` directory. 132 | 133 | Now you can host the static site locally, by using `serve` for example 134 | 135 | ```sh 136 | serve -s build -l 80 137 | ``` 138 | 139 | This will serve the build folder on port 80 of your machine meaning it can be retrieved via a browser by either going to your machines public IP or hostname 140 | 141 | 142 | 143 | 144 | 145 | ## Roadmap 146 | 147 | See the [open issues](https://github.com/GRVYDEV/Lightspeed-react/issues) for a list of proposed features (and known issues). 148 | 149 | 150 | 151 | ## Contributing 152 | 153 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 154 | 155 | 1. Fork the Project 156 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 157 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 158 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 159 | 5. Open a Pull Request 160 | 161 | 162 | 163 | ## License 164 | 165 | Distributed under the MIT License. See `LICENSE` for more information. 166 | 167 | 168 | 169 | ## Contact 170 | 171 | Garrett Graves - [@grvydev](https://twitter.com/grvydev) 172 | 173 | Project Link: [https://github.com/GRVYDEV/Lightspeed-react](https://github.com/GRVYDEV/Lightspeed-react) 174 | 175 | 176 | 177 | ## Acknowledgements 178 | 179 | - [Sean Dubois](https://github.com/Sean-Der) 180 | 181 | 182 | 183 | 184 | 185 | 186 | --------------------------------------------------------------------------------