├── 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 |
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 | You need to enable JavaScript to run this app.
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 |
4 |
5 |
6 | Project Lightspeed React [Deprecated]
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
37 | About The Project
38 |
41 |
42 |
43 | Getting Started
44 |
48 |
49 | Usage
50 | Roadmap
51 | Contributing
52 | License
53 | Contact
54 | Acknowledgements
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 |
--------------------------------------------------------------------------------