├── frontend ├── 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 │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── images │ │ ├── videoPoster.jpg │ │ └── lightspeedlogo.svg │ ├── manifest.json │ └── index.html ├── docker │ ├── config.json.template │ └── entrypoint.sh ├── .dockerignore ├── .eslintrc.json ├── Dockerfile ├── package.json ├── images │ └── lightspeedlogo.svg └── README.md ├── webrtc ├── .dockerignore ├── ws │ ├── message.go │ ├── hub.go │ └── client.go ├── go.mod ├── .gitignore ├── Dockerfile ├── internal │ └── signal │ │ ├── rand.go │ │ ├── http.go │ │ ├── h264.go │ │ ├── signal.go │ │ └── nalunittype.go ├── images │ └── lightspeedlogo.svg ├── README.md ├── main.go └── go.sum ├── ingest ├── .dockerignore ├── .gitignore ├── Dockerfile ├── Cargo.toml ├── src │ ├── cli.yml │ ├── main.rs │ ├── ftl_codec.rs │ └── connection.rs ├── images │ └── lightspeedlogo.svg ├── README.md └── Cargo.lock ├── images ├── streamkey-example.png ├── Lightspeed-Diagram.jpeg └── lightspeedlogo.svg ├── .github ├── workflows │ ├── webrtc.yml │ └── ingest.yml └── FUNDING.yml ├── .env ├── LICENSE ├── docker-compose.yml ├── contrib └── ubuntu_installer │ ├── README.md │ └── ubuntu_installer.sh └── README.md /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webrtc/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | README.md 3 | LICENSE -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .idea/ 3 | .DS_Store 4 | .eslintcache 5 | /build/ -------------------------------------------------------------------------------- /frontend/public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "wsUrl": "ws://localhost:8080/websocket" 3 | } 4 | -------------------------------------------------------------------------------- /ingest/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Dockerfile 3 | README.md 4 | LICENSE 5 | .github/ 6 | -------------------------------------------------------------------------------- /frontend/docker/config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "wsUrl": "${WEBSOCKET_URL}/websocket" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dockerignore 3 | .gitignore 4 | Dockerfile 5 | README.md 6 | LICENSE -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /images/streamkey-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/HEAD/images/streamkey-example.png -------------------------------------------------------------------------------- /frontend/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | envsubst < /config.json.template > "/usr/share/nginx/html/config.json" 4 | -------------------------------------------------------------------------------- /images/Lightspeed-Diagram.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/HEAD/images/Lightspeed-Diagram.jpeg -------------------------------------------------------------------------------- /frontend/public/images/videoPoster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/HEAD/frontend/public/images/videoPoster.jpg -------------------------------------------------------------------------------- /frontend/src/assets/constants.js: -------------------------------------------------------------------------------- 1 | export const LightspeedLogoURL = "/images/lightspeedlogo.svg"; 2 | export const VideoPosterURL = "/images/videoPoster.jpg"; 3 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /ingest/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # Hash file for password 10 | hash 11 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /webrtc/ws/message.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import "encoding/json" 4 | 5 | const ( 6 | MessageTypeAnswer = "answer" 7 | MessageTypeCandidate = "candidate" 8 | MessageTypeOffer = "offer" 9 | MessageTypeInfo = "info" 10 | ) 11 | 12 | type WebsocketMessage struct { 13 | Event string `json:"event"` 14 | Data json.RawMessage `json:"data"` 15 | } 16 | -------------------------------------------------------------------------------- /webrtc/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GRVYDEV/lightspeed-webrtc 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.2 7 | github.com/pion/interceptor v0.1.10 8 | github.com/pion/randutil v0.1.0 9 | github.com/pion/rtp v1.7.12 10 | github.com/pion/webrtc/v3 v3.1.28 11 | github.com/pkg/errors v0.9.1 // indirect 12 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /webrtc/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.exec 8 | .DS_Store 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # ide 19 | /.idea/ 20 | # actual binary 21 | /lightspeed-webrtc -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /ingest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest as builder 2 | WORKDIR /rust/src/ 3 | COPY . . 4 | RUN cargo install --path . 5 | 6 | 7 | FROM debian:buster-slim as lightspeed-ingest 8 | RUN useradd -M -s /bin/bash lightspeed 9 | WORKDIR /data 10 | RUN chown lightspeed:root /data 11 | COPY --from=builder --chown=lightspeed:lightspeed /usr/local/cargo/bin/lightspeed-ingest /usr/local/bin/lightspeed-ingest 12 | 13 | USER lightspeed 14 | CMD ["lightspeed-ingest"] 15 | 16 | EXPOSE 8084 -------------------------------------------------------------------------------- /webrtc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 as builder 2 | 3 | WORKDIR /go/src/app 4 | COPY . . 5 | ENV GO111MODULE=on 6 | RUN go mod download 7 | RUN go build -o lightspeed-webrtc . 8 | 9 | 10 | FROM debian:buster-slim 11 | COPY --from=builder /go/src/app/lightspeed-webrtc /usr/local/bin/ 12 | EXPOSE 8080 13 | 14 | #CMD ["lightspeed-webrtc --addr=XXX.XXX.XXX.XXX", "run"] 15 | # defaults to localhost:8080, then up to docker compose to bind ports 16 | CMD ["lightspeed-webrtc", "--addr=localhost"] 17 | -------------------------------------------------------------------------------- /.github/workflows/webrtc.yml: -------------------------------------------------------------------------------- 1 | name: Webrtc Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./webrtc 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.15 21 | - name: Build 22 | run: go build -v ./... 23 | -------------------------------------------------------------------------------- /frontend/.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 | -------------------------------------------------------------------------------- /webrtc/internal/signal/rand.go: -------------------------------------------------------------------------------- 1 | package signal 2 | 3 | import "github.com/pion/randutil" 4 | 5 | // RandSeq generates a random string to serve as dummy data 6 | // 7 | // It returns a deterministic sequence of values each time a program is run. 8 | // Use rand.Seed() function in your real applications. 9 | func RandSeq(n int) string { 10 | val, err := randutil.GenerateCryptoRandomString(n, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | return val 16 | } -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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_URL=ws://localhost:8080 15 | EXPOSE 80/tcp 16 | COPY --chown=1000 docker/entrypoint.sh /docker-entrypoint.d/entrypoint.sh 17 | COPY --chown=1000 docker/config.json.template /config.json.template 18 | COPY --from=builder --chown=1000 /app/Lightspeed-react/build /usr/share/nginx/html 19 | -------------------------------------------------------------------------------- /ingest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lightspeed-ingest" 3 | version = "0.1.0" 4 | authors = ["Garrett Graves "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bytes = "1.0.0" 11 | clap = {version = "2.33.3", features = ["yaml"]} 12 | futures = "0.3.8" 13 | futures-util = "0.3.8" 14 | hex = "0.4.2" 15 | log = "0.4.0" 16 | rand = "0.8.1" 17 | regex = "1" 18 | ring = "0.16.19" 19 | rtp-rs = "0.5.0" 20 | simplelog = "^0.7.6" 21 | tokio = { version = "1.0.1", features = ["full"] } 22 | tokio-util = { version = "0.6.0", features = ["codec"] } 23 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [GRVYDEV] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /webrtc/internal/signal/http.go: -------------------------------------------------------------------------------- 1 | package signal 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | // HTTPSDPServer starts a HTTP Server that consumes SDPs 12 | func HTTPSDPServer() chan string { 13 | port := flag.Int("port", 8080, "http server port") 14 | flag.Parse() 15 | 16 | sdpChan := make(chan string) 17 | http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { 18 | body, _ := ioutil.ReadAll(r.Body) 19 | fmt.Fprintf(w, "done") 20 | sdpChan <- string(body) 21 | }) 22 | 23 | go func() { 24 | err := http.ListenAndServe(":"+strconv.Itoa(*port), nil) 25 | if err != nil { 26 | panic(err) 27 | } 28 | }() 29 | 30 | return sdpChan 31 | } -------------------------------------------------------------------------------- /webrtc/internal/signal/h264.go: -------------------------------------------------------------------------------- 1 | package signal 2 | 3 | type NAL struct { 4 | PictureOrderCount uint32 5 | 6 | // NAL header 7 | ForbiddenZeroBit bool 8 | RefIdc uint8 9 | UnitType NalUnitType 10 | 11 | Data []byte // header byte + rbsp 12 | } 13 | 14 | func NewNal(data []byte) *NAL { 15 | return &NAL{PictureOrderCount: 0, ForbiddenZeroBit: false, RefIdc: 0, UnitType: NalUnitTypeUnspecified, Data: data} 16 | } 17 | 18 | func (h *NAL) ParseHeader() { 19 | firstByte := h.Data[0] 20 | h.ForbiddenZeroBit = (((firstByte & 0x80) >> 7) == 1) // 0x80 = 0b10000000 21 | h.RefIdc = (firstByte & 0x60) >> 5 // 0x60 = 0b01100000 22 | h.UnitType = NalUnitType((firstByte & 0x1F) >> 0) // 0x1F = 0b00011111 23 | } -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | videoRef.current.play(); 13 | } 14 | }, [src]); 15 | 16 | return ( 17 | 25 | ); 26 | }; 27 | 28 | export default VideoPlayer; 29 | 30 | VideoPlayer.propTypes = { 31 | src: PropTypes.object, 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Environment Variables for all docker containers 2 | # Be sure to check the individual repo's for a full list of variables that may change over time 3 | 4 | # WEBSOCKET_HOST could be your local IP (for development) or your Public IP (for production) NOTE: Hostname WILL NOT work 5 | WEBSOCKET_HOST= 6 | WEBSOCKET_PORT=8080 7 | 8 | # Websocket URL. Use `wss://` if you are using SSL 9 | WEBSOCKET_URL="ws://${WEBSOCKET_HOST}:${WEBSOCKET_PORT}" 10 | 11 | WEB_PORT=8888 12 | 13 | # A comma separated list of ICE / STUN servers. Availiable servers can be found from: 14 | # - https://gist.github.com/sagivo/3a4b2f2c7ac6e1b5267c2f1f59ac6c6b 15 | # - Searching for "public ice servers" in the search engine of your choice 16 | ICE_SERVERS=stun:stun.l.google.com:19302,stun:stun1.l.google.com:19302,stun:stun.stunprotocol.org:3478 17 | 18 | INGEST_PORT=8084 19 | #Optionally hardcode a stream key (it will be prefixed by "77-") 20 | #STREAM_KEY= 21 | 22 | -------------------------------------------------------------------------------- /ingest/src/cli.yml: -------------------------------------------------------------------------------- 1 | name: Lightspeed Ingest 2 | version: "0.1.0" 3 | author: "Garrett Graves " 4 | about: "A FTL handshake server written in Rust" 5 | args: 6 | - address: 7 | short: a 8 | long: address 9 | env: LS_INGEST_ADDR 10 | value_name: HOSTNAME_OR_IP 11 | help: Specify which address to bind to (defaults to 0.0.0.0) 12 | takes_value: true 13 | 14 | - stream-key: 15 | short: k 16 | long: stream-key 17 | env: STREAM_KEY 18 | value_name: STREAM_KEY 19 | help: Optionally set a static Stream Key (will be prefixed with "77-") 20 | takes_value: true 21 | 22 | # Optional path to the log file, creates a simplelog::WriteLogger 23 | - log-file: 24 | short: l 25 | long: log-file 26 | env: LS_LOG 27 | value_name: LOG_FILE_PATH 28 | help: Optionally specify where to store logs 29 | takes_value: true 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /.github/workflows/ingest.yml: -------------------------------------------------------------------------------- 1 | name: Ingest Lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint: 11 | name: CI 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: ./ingest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | - name: Add clippy 24 | run: rustup component add clippy 25 | - name: Add rustfmt 26 | run: rustup component add rustfmt 27 | - name: Check 28 | uses: actions-rs/cargo@v1 29 | with: 30 | args: --manifest-path ./ingest/Cargo.toml 31 | command: check 32 | - name: Fmt 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: fmt 36 | args: --all --manifest-path ./ingest/Cargo.toml -- --check 37 | - name: Clippy 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: clippy 41 | args: --manifest-path ./ingest/Cargo.toml -- -D warnings -A clippy::needless_return -A clippy::upper-case-acronyms 42 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/images/lightspeedlogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ingest/images/lightspeedlogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webrtc/images/lightspeedlogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/images/lightspeedlogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | lightspeed-ingest: 5 | container_name: lightspeed-ingest 6 | image: projectlightspeed/ingest 7 | #Uncomment below to build locally 8 | # image: lightspeed-ingest 9 | # build: 10 | # context: ./ingest 11 | # dockerfile: Dockerfile 12 | env_file: '.env' 13 | restart: on-failure 14 | ports: 15 | - "${INGEST_PORT}:8084" 16 | 17 | lightspeed-react: 18 | container_name: lightspeed-react 19 | image: projectlightspeed/react 20 | #Uncomment below to build locally 21 | # image: lightspeed-react 22 | # build: 23 | # context: ./frontend 24 | # dockerfile: Dockerfile 25 | env_file: '.env' 26 | restart: on-failure 27 | ports: 28 | - "${WEB_PORT}:80" 29 | 30 | lightspeed-webrtc: 31 | container_name: lightspeed-webrtc 32 | image: projectlightspeed/webrtc 33 | #Uncomment below to build locally 34 | # image: lightspeed-webrtc 35 | # build: 36 | # context: ./webrtc 37 | # dockerfile: Dockerfile 38 | env_file: '.env' 39 | command: ["lightspeed-webrtc", "--addr=0.0.0.0", "--ip=${WEBSOCKET_HOST}", "--ports=20000-20100", "--ice-servers=${ICE_SERVERS}", "run"] 40 | restart: on-failure 41 | ports: 42 | - ${WEBSOCKET_PORT}:8080 # WebRTC 43 | - 65535:65535/udp # RTP 44 | - 20000-20100:20000-20100/tcp # WebRTC PeerConnection 45 | - 20000-20100:20000-20100/udp # WebRTC PeerConnection UDP 46 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | `; -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /ingest/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate log; 4 | extern crate simplelog; 5 | use clap::App; 6 | use log::info; 7 | use simplelog::*; 8 | 9 | mod connection; 10 | mod ftl_codec; 11 | use std::fs::File; 12 | use tokio::net::TcpListener; 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<(), Box> { 16 | let default_bind_address = "0.0.0.0"; 17 | // update cli.yml to add more flags 18 | let cli_cfg = load_yaml!("cli.yml"); 19 | let matches = App::from_yaml(cli_cfg).get_matches(); 20 | 21 | // Find an address and port to bind to. The search order is as follows: 22 | // 1.) command line argument 23 | // 2.) environment variable (LS_INGEST_ADDR) 24 | // 3.) Default to 0.0.0.0 25 | let bind_address: &str = match matches.value_of("address") { 26 | Some(addr) => { 27 | if addr.is_empty() { 28 | default_bind_address 29 | } else { 30 | addr 31 | } 32 | } 33 | None => default_bind_address, 34 | }; 35 | 36 | let mut loggers: Vec> = 37 | vec![ 38 | match TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed) { 39 | Some(termlogger) => termlogger, 40 | None => SimpleLogger::new(LevelFilter::Info, Config::default()), 41 | }, 42 | ]; 43 | if let Some(path) = matches.value_of("log-file") { 44 | if !path.is_empty() { 45 | loggers.push(WriteLogger::new( 46 | LevelFilter::Info, 47 | Config::default(), 48 | File::create(path).unwrap(), 49 | )) 50 | } 51 | }; 52 | let _ = CombinedLogger::init(loggers); 53 | 54 | let stream_key_env = matches.value_of("stream-key"); 55 | let _ = connection::read_stream_key(true, stream_key_env); 56 | info!("Listening on {}:8084", bind_address); 57 | let listener = TcpListener::bind(format!("{}:8084", bind_address)).await?; 58 | 59 | loop { 60 | // Wait until someone tries to connect then handle the connection in a new task 61 | let (socket, _) = listener.accept().await?; 62 | tokio::spawn(async move { 63 | connection::Connection::init(socket); 64 | // handle_connection(socket).await; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /webrtc/internal/signal/signal.go: -------------------------------------------------------------------------------- 1 | // Package signal contains helpers to exchange the SDP session 2 | // description between examples. 3 | package signal 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "strings" 16 | ) 17 | 18 | // Allows compressing offer/answer to bypass terminal input limits. 19 | const compress = false 20 | 21 | // MustReadStdin blocks until input is received from stdin 22 | func MustReadStdin() string { 23 | r := bufio.NewReader(os.Stdin) 24 | 25 | var in string 26 | for { 27 | var err error 28 | in, err = r.ReadString('\n') 29 | if err != io.EOF { 30 | if err != nil { 31 | panic(err) 32 | } 33 | } 34 | in = strings.TrimSpace(in) 35 | if len(in) > 0 { 36 | break 37 | } 38 | } 39 | 40 | fmt.Println("") 41 | 42 | return in 43 | } 44 | 45 | // Encode encodes the input in base64 46 | // It can optionally zip the input before encoding 47 | func Encode(obj interface{}) string { 48 | b, err := json.Marshal(obj) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | if compress { 54 | b = zip(b) 55 | } 56 | 57 | return base64.StdEncoding.EncodeToString(b) 58 | } 59 | 60 | // Decode decodes the input from base64 61 | // It can optionally unzip the input after decoding 62 | func Decode(in string, obj interface{}) { 63 | b, err := base64.StdEncoding.DecodeString(in) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | if compress { 69 | b = unzip(b) 70 | } 71 | 72 | err = json.Unmarshal(b, obj) 73 | if err != nil { 74 | panic(err) 75 | } 76 | } 77 | 78 | func zip(in []byte) []byte { 79 | var b bytes.Buffer 80 | gz := gzip.NewWriter(&b) 81 | _, err := gz.Write(in) 82 | if err != nil { 83 | panic(err) 84 | } 85 | err = gz.Flush() 86 | if err != nil { 87 | panic(err) 88 | } 89 | err = gz.Close() 90 | if err != nil { 91 | panic(err) 92 | } 93 | return b.Bytes() 94 | } 95 | 96 | func unzip(in []byte) []byte { 97 | var b bytes.Buffer 98 | _, err := b.Write(in) 99 | if err != nil { 100 | panic(err) 101 | } 102 | r, err := gzip.NewReader(&b) 103 | if err != nil { 104 | panic(err) 105 | } 106 | res, err := ioutil.ReadAll(r) 107 | if err != nil { 108 | panic(err) 109 | } 110 | return res 111 | } -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webrtc/ws/hub.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | const ( 11 | maxMessageSize = 4096 12 | pongWait = 2 * time.Minute 13 | pingPeriod = time.Minute 14 | writeWait = 10 * time.Second 15 | ) 16 | 17 | type Info struct { 18 | NoConnections int `json:"no_connections"` 19 | } 20 | 21 | type Hub struct { 22 | // Registered Clients. 23 | Clients map[*Client]struct{} 24 | 25 | // Broadcast messages to all Clients. 26 | Broadcast chan []byte 27 | 28 | // Register a new client to the hub. 29 | Register chan *Client 30 | 31 | // Unregister a client from the hub. 32 | Unregister chan *Client 33 | 34 | // lock to prevent write to closed channel 35 | sync.RWMutex 36 | } 37 | 38 | func NewHub() *Hub { 39 | return &Hub{ 40 | Clients: make(map[*Client]struct{}), 41 | Broadcast: make(chan []byte), 42 | Register: make(chan *Client, 1), 43 | Unregister: make(chan *Client, 1), 44 | } 45 | } 46 | 47 | // NoClients returns the number of Clients registered 48 | func (h *Hub) NoClients() int { 49 | h.RLock() 50 | defer h.RUnlock() 51 | return len(h.Clients) 52 | } 53 | 54 | // Run is the main hub event loop handling register, unregister and broadcast events. 55 | func (h *Hub) Run() { 56 | for { 57 | select { 58 | case client := <-h.Register: 59 | h.Lock() 60 | h.Clients[client] = struct{}{} 61 | h.Unlock() 62 | go h.SendInfo(h.GetInfo()) 63 | case client := <-h.Unregister: 64 | h.RLock() 65 | if _, ok := h.Clients[client]; ok { 66 | h.RUnlock() 67 | h.Lock() 68 | delete(h.Clients, client) 69 | h.Unlock() 70 | client.conn.Close() 71 | close(client.Send) 72 | go h.SendInfo(h.GetInfo()) // this way the number of Clients does not change between calling the goroutine and executing it 73 | } else { 74 | h.RUnlock() 75 | } 76 | case message := <-h.Broadcast: 77 | h.RLock() 78 | for client := range h.Clients { 79 | client.Send <- message 80 | } 81 | h.RUnlock() 82 | } 83 | } 84 | } 85 | 86 | func (h *Hub) GetInfo() Info { 87 | return Info{ 88 | NoConnections: h.NoClients(), 89 | } 90 | } 91 | 92 | // SendInfo broadcasts hub statistics to all Clients. 93 | func (h *Hub) SendInfo(info Info) { 94 | i, err := json.Marshal(info) 95 | if err != nil { 96 | log.Printf("could not marshal ws info: %s", err) 97 | } 98 | if msg, err := json.Marshal(WebsocketMessage{ 99 | Event: MessageTypeInfo, 100 | Data: i, 101 | }); err == nil { 102 | h.Broadcast <- msg 103 | } else { 104 | log.Printf("could not marshal ws message: %s", err) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /webrtc/internal/signal/nalunittype.go: -------------------------------------------------------------------------------- 1 | package signal 2 | 3 | import "strconv" 4 | 5 | // NalUnitType is the type of a NAL 6 | type NalUnitType uint8 7 | 8 | // Enums for NalUnitTypes 9 | const ( 10 | NalUnitTypeUnspecified NalUnitType = 0 // Unspecified 11 | NalUnitTypeCodedSliceNonIdr NalUnitType = 1 // Coded slice of a non-IDR picture 12 | NalUnitTypeCodedSliceDataPartitionA NalUnitType = 2 // Coded slice data partition A 13 | NalUnitTypeCodedSliceDataPartitionB NalUnitType = 3 // Coded slice data partition B 14 | NalUnitTypeCodedSliceDataPartitionC NalUnitType = 4 // Coded slice data partition C 15 | NalUnitTypeCodedSliceIdr NalUnitType = 5 // Coded slice of an IDR picture 16 | NalUnitTypeSEI NalUnitType = 6 // Supplemental enhancement information (SEI) 17 | NalUnitTypeSPS NalUnitType = 7 // Sequence parameter set 18 | NalUnitTypePPS NalUnitType = 8 // Picture parameter set 19 | NalUnitTypeAUD NalUnitType = 9 // Access unit delimiter 20 | NalUnitTypeEndOfSequence NalUnitType = 10 // End of sequence 21 | NalUnitTypeEndOfStream NalUnitType = 11 // End of stream 22 | NalUnitTypeFiller NalUnitType = 12 // Filler data 23 | NalUnitTypeSpsExt NalUnitType = 13 // Sequence parameter set extension 24 | NalUnitTypeCodedSliceAux NalUnitType = 19 // Coded slice of an auxiliary coded picture without partitioning 25 | // 14..18 // Reserved 26 | // 20..23 // Reserved 27 | // 24..31 // Unspecified 28 | ) 29 | 30 | func (n *NalUnitType) String() string { 31 | var str string 32 | switch *n { 33 | case NalUnitTypeUnspecified: 34 | str = "Unspecified" 35 | case NalUnitTypeCodedSliceNonIdr: 36 | str = "CodedSliceNonIdr" 37 | case NalUnitTypeCodedSliceDataPartitionA: 38 | str = "CodedSliceDataPartitionA" 39 | case NalUnitTypeCodedSliceDataPartitionB: 40 | str = "CodedSliceDataPartitionB" 41 | case NalUnitTypeCodedSliceDataPartitionC: 42 | str = "CodedSliceDataPartitionC" 43 | case NalUnitTypeCodedSliceIdr: 44 | str = "CodedSliceIdr" 45 | case NalUnitTypeSEI: 46 | str = "SEI" 47 | case NalUnitTypeSPS: 48 | str = "SPS" 49 | case NalUnitTypePPS: 50 | str = "PPS" 51 | case NalUnitTypeAUD: 52 | str = "AUD" 53 | case NalUnitTypeEndOfSequence: 54 | str = "EndOfSequence" 55 | case NalUnitTypeEndOfStream: 56 | str = "EndOfStream" 57 | case NalUnitTypeFiller: 58 | str = "Filler" 59 | case NalUnitTypeSpsExt: 60 | str = "SpsExt" 61 | case NalUnitTypeCodedSliceAux: 62 | str = "NalUnitTypeCodedSliceAux" 63 | default: 64 | str = "Unknown" 65 | } 66 | str = str + "(" + strconv.FormatInt(int64(*n), 10) + ")" 67 | return str 68 | } -------------------------------------------------------------------------------- /frontend/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 "track": { 14 | state.stream.addTrack(action.track); 15 | return { ...state, stream: state.stream }; 16 | } 17 | case "info": { 18 | return { ...state, viewers: action.viewers }; 19 | } 20 | 21 | default: { 22 | return { ...state }; 23 | } 24 | } 25 | }; 26 | 27 | const initialState = { 28 | stream: new MediaStream(), 29 | viewers: null, 30 | }; 31 | 32 | const App = () => { 33 | const [state, dispatch] = useReducer(appReducer, initialState); 34 | const { pc } = useRTC(); 35 | const { socket } = useSocket(); 36 | 37 | pc.ontrack = (event) => { 38 | const { track } = event; 39 | dispatch({ type: "track", track: track }); 40 | }; 41 | 42 | pc.onicecandidate = (e) => { 43 | const { candidate } = e; 44 | if (candidate) { 45 | console.log("Candidate success"); 46 | socket.send( 47 | JSON.stringify({ 48 | event: "candidate", 49 | data: e.candidate, 50 | }) 51 | ); 52 | } 53 | }; 54 | 55 | if (socket) { 56 | socket.onmessage = async (event) => { 57 | const msg = JSON.parse(event.data); 58 | 59 | if (!msg) { 60 | console.log("Failed to parse msg"); 61 | return; 62 | } 63 | 64 | const offerCandidate = msg.data; 65 | 66 | if (!offerCandidate) { 67 | console.log("Failed to parse offer msg data"); 68 | return; 69 | } 70 | 71 | switch (msg.event) { 72 | case "offer": 73 | console.log("Offer"); 74 | pc.setRemoteDescription(offerCandidate); 75 | 76 | try { 77 | const answer = await pc.createAnswer(); 78 | pc.setLocalDescription(answer); 79 | socket.send( 80 | JSON.stringify({ 81 | event: "answer", 82 | data: answer, 83 | }) 84 | ); 85 | } catch (e) { 86 | console.error(e.message); 87 | } 88 | 89 | return; 90 | case "candidate": 91 | console.log("Candidate"); 92 | pc.addIceCandidate(offerCandidate); 93 | return; 94 | case "info": 95 | dispatch({ 96 | type: "info", 97 | viewers: msg.data.no_connections, 98 | }); 99 | } 100 | }; 101 | } 102 | 103 | return ( 104 | <> 105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | }; 116 | 117 | export default App; 118 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /webrtc/ws/client.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/pion/webrtc/v3" 10 | ) 11 | 12 | // Client is a middleman between the websocket connection and the hub. 13 | type Client struct { 14 | hub *Hub 15 | 16 | // The websocket connection. 17 | conn *websocket.Conn 18 | 19 | // Buffered channel of outbound messages. 20 | Send chan []byte 21 | 22 | // webRTC peer connection 23 | PeerConnection *webrtc.PeerConnection 24 | } 25 | 26 | func NewClient(hub *Hub, conn *websocket.Conn, webrtcConn *webrtc.PeerConnection) *Client { 27 | return &Client{ 28 | hub: hub, 29 | conn: conn, 30 | Send: make(chan []byte), 31 | PeerConnection: webrtcConn, 32 | } 33 | } 34 | 35 | // ReadLoop pumps messages from the websocket connection to the hub. 36 | // 37 | // The application runs ReadLoop in a per-connection goroutine. The application 38 | // ensures that there is at most one reader on a connection by executing all 39 | // reads from this goroutine. 40 | func (c *Client) ReadLoop() { 41 | defer func() { 42 | c.hub.Unregister <- c 43 | c.conn.Close() 44 | }() 45 | c.conn.SetReadLimit(maxMessageSize) 46 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 47 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 48 | message := &WebsocketMessage{} 49 | for { 50 | // _, message, err := c.conn.ReadMessage() 51 | _, raw, err := c.conn.ReadMessage() 52 | if err != nil { 53 | log.Printf("could not read message: %s", err) 54 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 55 | log.Println("ws closed unexpected") 56 | } 57 | return 58 | } 59 | 60 | err = json.Unmarshal(raw, &message) 61 | if err != nil { 62 | log.Printf("could not unmarshal ws message: %s", err) 63 | return 64 | } 65 | 66 | switch message.Event { 67 | case MessageTypeCandidate: 68 | candidate := webrtc.ICECandidateInit{} 69 | if err := json.Unmarshal(message.Data, &candidate); err != nil { 70 | log.Printf("could not unmarshal candidate msg: %s", err) 71 | return 72 | } 73 | 74 | if err := c.PeerConnection.AddICECandidate(candidate); err != nil { 75 | log.Printf("could not add ice candidate: %s", err) 76 | return 77 | } 78 | 79 | case MessageTypeAnswer: 80 | answer := webrtc.SessionDescription{} 81 | if err := json.Unmarshal(message.Data, &answer); err != nil { 82 | log.Printf("could not unmarshal answer msg: %s", err) 83 | return 84 | } 85 | 86 | if err := c.PeerConnection.SetRemoteDescription(answer); err != nil { 87 | log.Printf("could not set remote description: %s", err) 88 | return 89 | } 90 | } 91 | } 92 | } 93 | 94 | // WriteLoop pumps messages from the hub to the websocket connection. 95 | // 96 | // A goroutine running WriteLoop is started for each connection. The 97 | // application ensures that there is at most one writer to a connection by 98 | // executing all writes from this goroutine. 99 | func (c *Client) WriteLoop() { 100 | ticker := time.NewTicker(pingPeriod) 101 | defer func() { 102 | ticker.Stop() 103 | c.conn.Close() 104 | }() 105 | for { 106 | select { 107 | case message, ok := <-c.Send: 108 | _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 109 | if !ok { 110 | // The hub closed the channel. 111 | _ = c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 112 | return 113 | } 114 | 115 | w, err := c.conn.NextWriter(websocket.TextMessage) 116 | if err != nil { 117 | return 118 | } 119 | _, err = w.Write(message) 120 | if err != nil { 121 | log.Printf("could not send message: %s",err) 122 | w.Close() 123 | return 124 | } 125 | 126 | if err := w.Close(); err != nil { 127 | return 128 | } 129 | 130 | case <-ticker.C: 131 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 132 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 133 | return 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ingest/src/ftl_codec.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, BufMut, BytesMut}; 2 | 3 | use std::collections::HashMap; 4 | use std::{fmt, io}; 5 | use tokio_util::codec::{Decoder, Encoder}; 6 | 7 | #[derive(Debug)] 8 | pub enum FtlCommand { 9 | HMAC, 10 | Connect { data: HashMap }, 11 | Ping, 12 | Dot, 13 | Attribute { data: HashMap }, 14 | Disconnect, 15 | } 16 | #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 17 | pub struct FtlCodec { 18 | command_buffer: std::vec::Vec, 19 | } 20 | 21 | impl FtlCodec { 22 | pub fn new() -> FtlCodec { 23 | FtlCodec { 24 | command_buffer: Vec::new(), 25 | } 26 | } 27 | 28 | pub fn reset(&mut self) { 29 | self.command_buffer = Vec::new(); 30 | } 31 | } 32 | 33 | impl Decoder for FtlCodec { 34 | type Item = FtlCommand; 35 | type Error = FtlError; 36 | fn decode(&mut self, buf: &mut BytesMut) -> Result, FtlError> { 37 | let command: String; 38 | let mut data: HashMap = HashMap::new(); 39 | match buf.windows(4).position(|window| window == b"\r\n\r\n") { 40 | Some(index) => { 41 | command = String::from_utf8_lossy(&buf[..index]).to_string(); 42 | buf.advance(index + 4); 43 | if command.as_str().contains("HMAC") { 44 | self.reset(); 45 | Ok(Some(FtlCommand::HMAC)) 46 | } else if command.as_str().contains("DISCONNECT") { 47 | self.reset(); 48 | Ok(Some(FtlCommand::Disconnect)) 49 | } else if command.as_str().contains("CONNECT") { 50 | let commands: Vec<&str> = command.split(' ').collect(); 51 | let mut key = commands[2].to_string(); 52 | key.remove(0); 53 | data.insert("channel_id".to_string(), commands[1].to_string()); 54 | data.insert("stream_key".to_string(), key); 55 | self.reset(); 56 | Ok(Some(FtlCommand::Connect { data })) 57 | } else if command.as_str().contains(':') { 58 | let commands: Vec<&str> = command.split(':').collect(); 59 | data.insert("key".to_string(), commands[0].to_string()); 60 | data.insert("value".to_string(), commands[1].trim().to_string()); 61 | self.reset(); 62 | Ok(Some(FtlCommand::Attribute { data })) 63 | } else if command.as_str().contains('.') && command.len() == 1 { 64 | self.reset(); 65 | Ok(Some(FtlCommand::Dot)) 66 | } else if command.as_str().contains("PING") { 67 | self.reset(); 68 | Ok(Some(FtlCommand::Ping)) 69 | } else { 70 | self.reset(); 71 | Err(FtlError::Unsupported(command)) 72 | } 73 | } 74 | None => Ok(None), 75 | } 76 | } 77 | } 78 | impl Encoder for FtlCodec 79 | where 80 | T: AsRef, 81 | { 82 | type Error = FtlError; 83 | 84 | fn encode(&mut self, line: T, buf: &mut BytesMut) -> Result<(), FtlError> { 85 | let line = line.as_ref(); 86 | buf.reserve(line.len()); 87 | buf.put(line.as_bytes()); 88 | Ok(()) 89 | } 90 | } 91 | #[derive(Debug)] 92 | pub enum FtlError { 93 | // ConnectionClosed, 94 | Unsupported(String), 95 | // CommandNotFound, 96 | Io(io::Error), 97 | } 98 | impl fmt::Display for FtlError { 99 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 100 | match self { 101 | // FtlError::ConnectionClosed => write!(f, "Connection Closed"), 102 | // FtlError::CommandNotFound => write!(f, "Command not read"), 103 | FtlError::Io(e) => write!(f, "{}", e), 104 | FtlError::Unsupported(s) => { 105 | write!(f, "Unsupported FTL Command {}! Bug GRVY to support this", s) 106 | } 107 | } 108 | } 109 | } 110 | impl From for FtlError { 111 | fn from(e: io::Error) -> FtlError { 112 | FtlError::Io(e) 113 | } 114 | } 115 | impl std::error::Error for FtlError {} 116 | -------------------------------------------------------------------------------- /contrib/ubuntu_installer/README.md: -------------------------------------------------------------------------------- 1 | # Project Lightspeed Ubuntu 20.04 installer 2 | 3 | > **Warning** 4 | > This script is designed for use on a **fresh install**. Please backup any existing configurations as some **will be overwritten** to install Lightspeed. 5 | > If you would like an alternate solution that does not require a fresh installation, please consult the [official wiki](https://github.com/GRVYDEV/Project-Lightspeed/README.md) for a docker based setup guide. 6 | 7 | Contained in this directory is a bash script to automatically install 8 | [GRVYDEV/Project-Lightspeed](https://github.com/GRVYDEV/Project-Lightspeed) on 9 | Ubuntu 20.04, compiled directly from source repositories, and install systemd 10 | services to run them. This installation method does not use Docker. 11 | 12 | ## Config 13 | 14 | The script can be configured directly via environment variables. Here is a list 15 | of the variables that you can configure: 16 | 17 | * `TLS_ON` - if set `true`, nginx will use port 443 and run TLS encrypted HTTPs 18 | services (html+websocket). Requests on port 80 will redirect to port 443. A 19 | TLS certificate will be generated and signed by Let's Encrypt. You must also 20 | set `DOMAIN` and `ACME_EMAIL`. 21 | * `DOMAIN` - The domain name you want to use for the stream website. This is 22 | only required if `TLS_ON=true`. 23 | * `ACME_EMAIL` - Your email address, that you want to use to register with 24 | Let's Encrypt. This is only required if `TLS_ON=true`. 25 | * `IP_ADDRESS` - The public IP address of the server. If you don't set this, 26 | the script will try to find this automatically. 27 | 28 | If you set `TLS_ON=true` (https:// for the stream website), the stream website 29 | requires a domain name, and you need a personal email address to register with 30 | Lets Encrypt. You will need to create a DNS `A` record for the domain pointing 31 | to the IP address of your server. If you don't know the IP address of the server 32 | until after you create it, you just need to be ready to create the DNS record 33 | quickly as soon as you know the IP address. certbot will run at the very end of 34 | the script, so you will have a few minutes with which to create the DNS record 35 | before certbot will need it to be ready. 36 | 37 | ## Run 38 | 39 | Example without TLS (no config necessary): 40 | 41 | ```bash 42 | #!/bin/bash 43 | 44 | curl -L https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/main/contrib/ubuntu_installer/ubuntu_installer.sh | sudo -E bash -xe 45 | ``` 46 | 47 | Example with TLS (config is set as env vars): 48 | 49 | ```bash 50 | #!/bin/bash 51 | 52 | export TLS_ON=true 53 | export DOMAIN=stream.example.com 54 | export ACME_EMAIL=email@example.com 55 | 56 | curl -L https://raw.githubusercontent.com/GRVYDEV/Project-Lightspeed/main/contrib/ubuntu_installer/ubuntu_installer.sh | sudo -E bash -xe 57 | ``` 58 | 59 | ## Get your stream key 60 | 61 | Once the script finishes, these new services will have been created: 62 | 63 | * `lightspeed-ingest` 64 | * `lightspeed-webrtc` 65 | * `nginx` 66 | 67 | In order to start streaming, you need the stream key, which is printed in the 68 | log for the `lightspeed-ingest` service. You can view the log this way: 69 | 70 | ```bash 71 | journalctl --unit lightspeed-ingest.service --no-pager 72 | ``` 73 | 74 | ## Run with cloud-init 75 | 76 | cloud-init lets you run this script on a new server, automatically, when you 77 | create the server. 78 | 79 | You can use this on DigitalOcean, or any other cloud host that supports 80 | cloud-init. Here's the directions for DigitalOcean: 81 | 82 | * Create an account or sign in with your existing one. 83 | * [Create a new droplet](https://cloud.digitalocean.com/droplets/new) 84 | * Choose a plan. This is tested to work on the smallest $5 plan. 85 | * Choose `Ubuntu 20.04 (LTS) x64` (default) 86 | * Under `Select additional options` check the box `User data`. 87 | * Enter the install script in the text area marked `Enter user data here...` 88 | * You can use any of the same examples Run from above. Make sure to include the 89 | first line `#!/bin/bash` and the exported variables you want (if any). 90 | * Review the rest of the droplet options and click Create Droplet. 91 | 92 | Now the droplet is being created. If you chose to set `TLS_ON=true`, you now 93 | need to copy the IP address of the new droplet, and create a DNS `A` record for 94 | your chosen `DOMAIN` (instructions vary depending on your domain DNS provider). 95 | 96 | Login to the droplet as root via SSH. (`ssh root@${IP_ADDRESS}`) 97 | 98 | From the server, you can watch the cloud-init script log file as it runs: 99 | 100 | ```bash 101 | tail -f /var/log/cloud-init-output.log 102 | ``` 103 | 104 | You can also show the status: 105 | 106 | ```bash 107 | cloud-init status -w 108 | ``` 109 | 110 | Using the `-w` argument, cloud-init will wait for the script to finish before 111 | printing anything. After waiting for the script to finish, it will print 112 | `status: done` or `status: error` depending on if the script ran successfully or 113 | not. 114 | 115 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 |

6 |

Project Lightspeed React

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 | A React website that connects to Lightspeed WebRTC via a websocket to negotiate SDPs and display a WebRTC stream. 19 | 20 | 21 |
22 |
23 | View Demo 24 | · 25 | Report Bug 26 | · 27 | Request Feature 28 |

29 |

30 | 31 | 32 |
33 |

Table of Contents

34 |
    35 |
  1. 36 | About The Project 37 | 40 |
  2. 41 |
  3. 42 | Getting Started 43 | 47 |
  4. 48 |
  5. Usage
  6. 49 |
  7. Roadmap
  8. 50 |
  9. Contributing
  10. 51 |
  11. License
  12. 52 |
  13. Contact
  14. 53 |
  15. Acknowledgements
  16. 54 |
55 |
56 | 57 | 58 | 59 | ## About The Project 60 | 61 | 62 | 63 | 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. 64 | 65 | ### Built With 66 | 67 | - React 68 | 69 | ### Dependencies 70 | 71 | - [Lightspeed WebRTC](https://github.com/GRVYDEV/Lightspeed-webrtc) 72 | - [Lightspeed Ingest](https://github.com/GRVYDEV/Lightspeed-ingest) 73 | 74 | 75 | 76 | ## Getting Started 77 | 78 | ## Setup 79 | 80 | ### Docker 81 | 82 | 1. Install [git](https://git-scm.com/downloads) 83 | 1. Build the image from the master branch with: 84 | 85 | ```sh 86 | docker build -t grvydev/lightspeed-react https://github.com/GRVYDEV/Lightspeed-react.git 87 | ``` 88 | 89 | 1. Run it with 90 | 91 | ```sh 92 | docker run -it --rm \ 93 | -p 8000:80/tcp \ 94 | -e WEBSOCKET_HOST=localhost \ 95 | -e WEBSOCKET_PORT=8080 \ 96 | grvydev/lightspeed-react 97 | ``` 98 | 99 | Where your websocket host from the browser/client perspective is accessible on `localhost:8080`. 100 | 101 | 1. You can now access it at [localhost:8000](http://localhost:8000). 102 | 103 | ### Locally 104 | 105 | To get a local copy up and running follow these simple steps. 106 | 107 | #### Prerequisites 108 | 109 | 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 110 | 111 | #### Installation 112 | 113 | ```sh 114 | git clone https://github.com/GRVYDEV/Lightspeed-react.git 115 | cd Lightspeed-react 116 | npm install 117 | ``` 118 | 119 | 120 | 121 | #### Usage 122 | 123 | First build the frontend 124 | 125 | ```sh 126 | cd Lightspeed-react 127 | npm run build 128 | ``` 129 | 130 | You should then configure the websocket URL in `config.json` in the `build` directory. 131 | 132 | Now you can host the static site locally, by using `serve` for example 133 | 134 | ```sh 135 | serve -s build -l 80 136 | ``` 137 | 138 | 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 139 | 140 | 141 | 142 | 143 | 144 | ## Roadmap 145 | 146 | See the [open issues](https://github.com/GRVYDEV/Lightspeed-react/issues) for a list of proposed features (and known issues). 147 | 148 | 149 | 150 | ## Contributing 151 | 152 | 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**. 153 | 154 | 1. Fork the Project 155 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 156 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 157 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 158 | 5. Open a Pull Request 159 | 160 | 161 | 162 | ## License 163 | 164 | Distributed under the MIT License. See `LICENSE` for more information. 165 | 166 | 167 | 168 | ## Contact 169 | 170 | Garrett Graves - [@grvydev](https://twitter.com/grvydev) 171 | 172 | Project Link: [https://github.com/GRVYDEV/Lightspeed-react](https://github.com/GRVYDEV/Lightspeed-react) 173 | 174 | 175 | 176 | ## Acknowledgements 177 | 178 | - [Sean Dubois](https://github.com/Sean-Der) 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /contrib/ubuntu_installer/ubuntu_installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | ## This is a bash script to install GRVYDEV/Project-Lightspeed on Ubuntu 20.04 4 | ## See the README for details: 5 | ## https://github.com/GRVYDEV/Project-Lightspeed/tree/main/contrib/ubuntu_installer 6 | 7 | 8 | lightspeed_config() { 9 | ## You can edit these defaults, or you can override them in your environment. 10 | ## Environment vars use the same names except without the DEFAULT_ prefix. 11 | 12 | # TLS is off by default. 13 | # Turn on HTTPS and proxy the websocket by setting TLS_ON=true 14 | DEFAULT_TLS_ON=false 15 | # YOUR email address to register Lets Encrypt account (only when TLS_ON=true) 16 | DEFAULT_ACME_EMAIL=email@example.com 17 | 18 | # Domain name for your stream website (only when TLS_ON=true): 19 | DEFAULT_DOMAIN=stream.example.com 20 | 21 | # Try to automatically find public IP address 22 | # Or you can just set IP_ADDRESS=x.x.x.x 23 | DEFAULT_IP_ADDRESS=$(curl ifconfig.co/) 24 | 25 | # Git repositories: 26 | DEFAULT_INGEST_REPO=https://github.com/GRVYDEV/Lightspeed-ingest.git 27 | DEFAULT_WEBRTC_REPO=https://github.com/GRVYDEV/Lightspeed-webrtc.git 28 | DEFAULT_REACT_REPO=https://github.com/GRVYDEV/Lightspeed-react.git 29 | 30 | # Git branch, tag, or commit to compile (default is HEAD from mainline branch): 31 | DEFAULT_INGEST_GIT_REF=main 32 | DEFAULT_WEBRTC_GIT_REF=main 33 | DEFAULT_REACT_GIT_REF=master 34 | 35 | # Directory to clone git repositories 36 | DEFAULT_GIT_ROOT=/root/git 37 | } 38 | 39 | lightspeed_install() { 40 | ## Load environment variables that possibly override default values: 41 | # (env vars are the same names as above, except without `DEFAULT_` prefix) 42 | TLS_ON=${TLS_ON:-$DEFAULT_TLS_ON} 43 | DOMAIN=${DOMAIN:-$DEFAULT_DOMAIN} 44 | IP_ADDRESS=${IP_ADDRESS:-$DEFAULT_IP_ADDRESS} 45 | INGEST_REPO=${INGEST_REPO:-$DEFAULT_INGEST_REPO} 46 | WEBRTC_REPO=${WEBRTC_REPO:-$DEFAULT_WEBRTC_REPO} 47 | REACT_REPO=${REACT_REPO:-$DEFAULT_REACT_REPO} 48 | INGEST_GIT_REF=${INGEST_GIT_REF:-$DEFAULT_INGEST_GIT_REF} 49 | WEBRTC_GIT_REF=${WEBRTC_GIT_REF:-$DEFAULT_WEBRTC_GIT_REF} 50 | REACT_GIT_REF=${REACT_GIT_REF:-$DEFAULT_REACT_GIT_REF} 51 | ACME_EMAIL=${ACME_EMAIL:-$DEFAULT_ACME_EMAIL} 52 | GIT_ROOT=${GIT_ROOT:-$DEFAULT_GIT_ROOT} 53 | 54 | if [ ${TLS_ON} = 'true' ]; then 55 | WEBRTC_IP_ADDRESS=${IP_ADDRESS} 56 | WEBSOCKET_URL=wss://${DOMAIN}/websocket 57 | else 58 | WEBRTC_IP_ADDRESS=${IP_ADDRESS} 59 | WEBSOCKET_URL=ws://${IP_ADDRESS}:8080/websocket 60 | fi 61 | 62 | export HOME=/root 63 | 64 | ## Install packages: 65 | export DEBIAN_FRONTEND=noninteractive 66 | apt-get update 67 | apt-get -y install \ 68 | golang \ 69 | git \ 70 | debian-keyring \ 71 | debian-archive-keyring \ 72 | apt-transport-https \ 73 | curl \ 74 | nginx \ 75 | certbot \ 76 | python3-certbot-nginx \ 77 | gcc \ 78 | libc6-dev 79 | 80 | ## Install latest nodejs and npm: 81 | curl -sL https://deb.nodesource.com/setup_15.x | bash - 82 | apt-get install -y nodejs 83 | 84 | ## Install latest rust version: 85 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 86 | source /root/.cargo/env 87 | 88 | ## Niceties: 89 | echo "set enable-bracketed-paste on" >> /root/.inputrc 90 | 91 | ## Install Project Lightspeed from source: 92 | # ingest: 93 | mkdir -p ${GIT_ROOT} 94 | cd ${GIT_ROOT} 95 | git clone ${INGEST_REPO} Lightspeed-ingest 96 | cd Lightspeed-ingest 97 | git checkout ${INGEST_GIT_REF} 98 | cargo build --release 99 | install target/release/lightspeed-ingest /usr/local/bin/lightspeed-ingest 100 | 101 | # webrtc: 102 | cd ${GIT_ROOT} 103 | git clone ${WEBRTC_REPO} Lightspeed-webrtc 104 | cd Lightspeed-webrtc 105 | git checkout ${WEBRTC_GIT_REF} 106 | GO111MODULE=on go build 107 | install lightspeed-webrtc /usr/local/bin/lightspeed-webrtc 108 | 109 | # react: 110 | cd ${GIT_ROOT} 111 | git clone ${REACT_REPO} Lightspeed-react 112 | cd Lightspeed-react 113 | git checkout ${REACT_GIT_REF} 114 | npm install 115 | npm run build 116 | mkdir -p /var/www/html 117 | cp -a build/* /var/www/html 118 | cat < /var/www/html/config.json 119 | { 120 | "wsUrl": "${WEBSOCKET_URL}" 121 | } 122 | EOF 123 | 124 | ## Create systemd service for ingest: 125 | 126 | cat < /etc/systemd/system/lightspeed-ingest.service 127 | [Unit] 128 | Description=Project Lightspeed ingest service 129 | After=network-online.target 130 | Wants=network-online.target 131 | [Service] 132 | TimeoutStartSec=0 133 | Environment=LS_INGEST_ADDR=${IP_ADDRESS} 134 | ExecStart=/usr/local/bin/lightspeed-ingest 135 | Restart=always 136 | RestartSec=60 137 | 138 | [Install] 139 | WantedBy=multi-user.target 140 | EOF 141 | 142 | ## Create systemd service for webrtc: 143 | 144 | cat < /etc/systemd/system/lightspeed-webrtc.service 145 | [Unit] 146 | Description=Project Lightspeed webrtc service 147 | After=network-online.target 148 | Wants=network-online.target 149 | [Service] 150 | TimeoutStartSec=0 151 | Environment=IP_ADDRESS=${WEBRTC_IP_ADDRESS} 152 | ExecStart=/usr/local/bin/lightspeed-webrtc --addr=@@@{IP_ADDRESS} 153 | Restart=always 154 | RestartSec=60 155 | 156 | [Install] 157 | WantedBy=multi-user.target 158 | EOF 159 | 160 | ## Install and start services: 161 | 162 | systemctl daemon-reload 163 | systemctl enable --now lightspeed-ingest 164 | systemctl enable --now lightspeed-webrtc 165 | 166 | ## Configure TLS with certbot: 167 | 168 | if [ ${TLS_ON} = 'true' ]; then 169 | certbot -n register --agree-tos -m ${ACME_EMAIL} 170 | certbot -n --nginx --domains ${DOMAIN} 171 | cat < /etc/nginx/sites-available/default 172 | server { 173 | listen 80 default_server; 174 | listen [::]:80 default_server; 175 | server_name _; 176 | return 301 https://@@@host@@@request_uri; 177 | } 178 | 179 | server { 180 | server_name ${DOMAIN}; 181 | listen 443 ssl; 182 | listen [::]:443 ssl ipv6only=on; 183 | root /var/www/html; 184 | index index.html; 185 | location / { 186 | # First attempt to serve request as file, then 187 | # as directory, then fall back to displaying a 404. 188 | try_files @@@uri @@@uri/ =404; 189 | } 190 | location /websocket { 191 | proxy_pass http://${IP_ADDRESS}:8080/websocket; 192 | proxy_http_version 1.1; 193 | proxy_set_header Upgrade @@@http_upgrade; 194 | proxy_set_header Connection "Upgrade"; 195 | proxy_set_header X-Real-IP @@@remote_addr; 196 | proxy_set_header X-Forwarded-For @@@proxy_add_x_forwarded_for; 197 | proxy_set_header Host @@@host; 198 | proxy_connect_timeout 24h; 199 | proxy_send_timeout 24h; 200 | proxy_read_timeout 24h; 201 | } 202 | ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; 203 | ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; 204 | include /etc/letsencrypt/options-ssl-nginx.conf; 205 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 206 | } 207 | EOF 208 | 209 | systemctl restart nginx 210 | fi 211 | } 212 | 213 | ## Configure and install: 214 | lightspeed_config 215 | lightspeed_install 216 | 217 | 218 | ## END 219 | -------------------------------------------------------------------------------- /ingest/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 |

6 |

Project Lightspeed Ingest

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 | A FTL handshake server written in Rust. This server listens on port 8084 and performs the FTL handshake with incoming connections 19 | 20 | 21 |
22 |
23 | View Demo 24 | · 25 | Report Bug 26 | · 27 | Request Feature 28 |

29 |

30 | 31 | 32 |
33 |

Table of Contents

34 |
    35 |
  1. 36 | About The Project 37 | 40 |
  2. 41 |
  3. 42 | Getting Started 43 | 47 |
  4. 48 |
  5. Usage
  6. 49 |
  7. Streaming From OBS 50 | 53 |
  8. 54 |
  9. Roadmap
  10. 55 |
  11. Contributing
  12. 56 |
  13. License
  14. 57 |
  15. Contact
  16. 58 |
  17. Acknowledgements
  18. 59 |
60 |
61 | 62 | 63 | 64 | ## About The Project 65 | 66 | 67 | 68 | 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 performs the FTL handshake with clients. It verifies the stream key and negotiates a port with the client connection that we will accept RTP packets on. In order for this to work the Project Lightspeed WebRTC is required in order to accept and broadcast the RTP packets. In order to view the live stream the Project Lightspeed React is required. 69 | 70 | ### Built With 71 | 72 | - Rust 73 | 74 | ### Dependencies 75 | 76 | - [Lightspeed WebRTC](https://github.com/GRVYDEV/Lightspeed-webrtc) 77 | - [Lightspeed React](https://github.com/GRVYDEV/Lightspeed-react) 78 | 79 | 80 | 81 | ## Getting Started 82 | 83 | To get a local copy up and running follow these simple steps. 84 | 85 | ### Prerequisites 86 | 87 | In order to run this Rust is required. Installation instructions can be found here. A C compiler is required as well. If you get a `linker cc not found error` try installing a C compiler 88 | 89 | ### Installation 90 | 91 | ```sh 92 | git clone https://github.com/GRVYDEV/Lightspeed-ingest.git 93 | cd Lightspeed-ingest 94 | cargo build 95 | ``` 96 | 97 | 98 | 99 | ## Usage 100 | To print out full command line usage information. 101 | 102 | ```sh 103 | cargo run -- -h 104 | ``` 105 | 106 | To run it with default settings type the following command. 107 | 108 | ```sh 109 | cargo run --release 110 | ``` 111 | 112 | To specify which address to bind to. 113 | 114 | ```sh 115 | cargo run --release -- -a 12.34.56.78 116 | ``` 117 | 118 | 119 | 120 | 121 | ## Streaming From OBS 122 | 123 | By default since we are using the FTL protocol you cannot just use a custom server. You will need to edit your `services.json` file. It can be found at `%AppData%\obs-studio\plugin_config\rtmp-services\services.json` on Windows and `/Users/YOURUSERNAME/Library/Application\ Support/obs-studio/plugin_config/rtmp-services/services.json` 124 | 125 | Paste this into the services array and change the url to either the IP or the hostname of your Project Lightspeed server 126 | ```json 127 | { 128 | "name": "Project Lightspeed", 129 | "common": false, 130 | "servers": [ 131 | { 132 | "name": "SERVER NAME HERE", 133 | "url": "your.lightspeed.hostname" 134 | } 135 | ], 136 | "recommended": { 137 | "keyint": 2, 138 | "output": "ftl_output", 139 | "max audio bitrate": 160, 140 | "max video bitrate": 8000, 141 | "profile": "main", 142 | "bframes": 0 143 | } 144 | }, 145 | ``` 146 | 147 | After restarting OBS you should be able to see your service in the OBS settings pane 148 | (Special Thanks to [Glimesh](https://github.com/Glimesh) for these instructions) 149 | 150 | 151 | ### Stream Key 152 | By default on first time startup a new stream key will be generated and output to the terminal for you. In order 153 | to regenerate this key simply delete the file it generates called `hash`. Simply copy the key output in the terminal 154 | to OBS and you are all set! This key WILL NOT change unless the `hash` file is deleted. 155 | 156 | You can assign a static key by passing `--stream-key mykey` or via environment variable `STREAM_KEY=mykey`. If you 157 | assign it manually it will become prefixed with `77-` so the result will be `77-mykey`. You can verify this in the boot 158 | logs. 159 | 160 | 161 | Streamkey example 162 | 163 | 164 | 165 | ## Roadmap 166 | 167 | See the [open issues](https://github.com/GRVYDEV/Lightspeed-ingest/issues) for a list of proposed features (and known issues). 168 | 169 | 170 | 171 | ## Contributing 172 | 173 | 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**. 174 | 175 | 1. Fork the Project 176 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 177 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 178 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 179 | 5. Open a Pull Request 180 | 181 | 182 | 183 | ## License 184 | 185 | Distributed under the MIT License. See `LICENSE` for more information. 186 | 187 | 188 | 189 | ## Contact 190 | 191 | Garrett Graves - [@grvydev](https://twitter.com/grvydev) 192 | 193 | Project Link: [https://github.com/GRVYDEV/Lightspeed-ingest](https://github.com/GRVYDEV/Lightspeed-ingest) 194 | 195 | 196 | 197 | ## Acknowledgements 198 | 199 | - [Sean Dubois](https://github.com/Sean-Der) 200 | - [Hayden McAfee](https://github.com/haydenmc) 201 | 202 | 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /webrtc/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 |

6 |

Project Lightspeed WebRTC

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 | A RTP -> WebRTC server based on Pion written in Go. This server accepts RTP packets on port 65535 and broadcasts them via WebRTC 19 | 20 | 21 |
22 |
23 | View Demo 24 | · 25 | Report Bug 26 | · 27 | Request Feature 28 |

29 |

30 | 31 | 32 |
33 |

Table of Contents

34 |
    35 |
  1. 36 | About The Project 37 | 40 |
  2. 41 |
  3. 42 | Getting Started 43 | 47 |
  4. 48 |
  5. Usage
  6. 49 |
  7. Roadmap
  8. 50 |
  9. Contributing
  10. 51 |
  11. License
  12. 52 |
  13. Contact
  14. 53 |
  15. Acknowledgements
  16. 54 |
55 |
56 | 57 | 58 | 59 | ## About The Project 60 | 61 | 62 | 63 | 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 takes RTP packets sent to the server and broadcasts them over WebRTC. In order for this to work the Project Lightspeed Ingest server is required to perfrom the FTL handshake with OBS. In order to view the live stream the Project Lightspeed viewer is required. 64 | 65 | ### Built With 66 | 67 | - Pion 68 | - Golang 69 | 70 | ### Dependencies 71 | 72 | - [Lightspeed Ingest](https://github.com/GRVYDEV/Lightspeed-ingest) 73 | - [Lightspeed React](https://github.com/GRVYDEV/Lightspeed-react) 74 | 75 | 76 | 77 | ## Getting Started 78 | 79 | To get a local copy up and running follow these simple steps. 80 | 81 | ### Prerequisites 82 | 83 | In order to run this Golang is required. Installation instructions can be found here 84 | 85 | ### Installation 86 | 87 | Using go get 88 | 89 | ```sh 90 | export GO111MODULE=on 91 | go get github.com/GRVYDEV/lightspeed-webrtc 92 | ``` 93 | 94 | Using git 95 | 96 | ```sh 97 | git clone https://github.com/GRVYDEV/Lightspeed-webrtc.git 98 | cd Lightspeed-webrtc 99 | go build 100 | ``` 101 | 102 | 103 | 104 | ## Usage 105 | 106 | To run type the following command. 107 | 108 | Using go get 109 | ```sh 110 | lightspeed-webrtc --addr=XXX.XXX.XXX.XXX 111 | ``` 112 | 113 | Using git 114 | ```sh 115 | cd Lightspeed-webrtc 116 | go build 117 | ./lightspeed-webrtc --addr=XXX.XXX.XXX.XXX 118 | ``` 119 | 120 | #### Arguments 121 | | Argument | Supported Values | Defaults | Notes | 122 | | :-------- | :--------------- | :------- | :---------------- | 123 | | `--addr` | A valid IP address | `localhost` | This is the local Ip address of your machine. It defaults to localhost but should be set to your local IP. For example 10.17.0.5 This is where the server will listen for UDP packets and where it will host the websocket endpoint for SDP negotiation| 124 | | `--ip` | A valid IP address | `none` | Sets the public IP address for WebRTC to use. This is especially useful in the context of Docker| 125 | | `--ports` | A valid UDP port range | `20000-20500` | This sets the UDP ports that WebRTC will use to connect with the client | 126 | | `--ws-port` | A valid port number | `8080` | This is the port on which the websocket will be hosted. If you change this value make sure that is reflected in the URL used by the react client | 127 | | `--rtp-port` | A valid port number | `65535` | This is the port on which the WebRTC service will listen for RTP packets. Ensure this is the same port that Lightspeed Ingest is negotiating with the client | 128 | | `--ssl-cert` | A valid ssl cert path | | This is the ssl cert that the websocket server will use. If omitted, the websocket will not be served over ssl. | 129 | | `--ssl-key` | A valid port number | | This is the ssl private key that the websocket server will use. If omitted, the websocket will not be served over ssl. | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | ## Roadmap 139 | 140 | See the [open issues](https://github.com/GRVYDEV/Lightspeed-webrtc/issues) for a list of proposed features (and known issues). 141 | 142 | 143 | 144 | ## Contributing 145 | 146 | 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**. 147 | 148 | 1. Fork the Project 149 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 150 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 151 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 152 | 5. Open a Pull Request 153 | 154 | 155 | 156 | ## License 157 | 158 | Distributed under the MIT License. See `LICENSE` for more information. 159 | 160 | 161 | 162 | ## Contact 163 | 164 | Garrett Graves - [@grvydev](https://twitter.com/grvydev) 165 | 166 | Project Link: [https://github.com/GRVYDEV/Lightspeed-webrtc](https://github.com/GRVYDEV/Lightspeed-webrtc) 167 | 168 | 169 | 170 | ## Acknowledgements 171 | 172 | - [Sean Dubois](https://github.com/Sean-Der) 173 | - [Hayden McAfee](https://github.com/haydenmc) 174 | 175 | 176 | 177 | 178 | 179 | [contributors-shield]: https://img.shields.io/github/contributors/GRVYDEV/repo.svg?style=for-the-badge 180 | [contributors-url]: https://github.com/GRVYDEV/repo/graphs/contributors 181 | [forks-shield]: https://img.shields.io/github/forks/GRVYDEV/repo.svg?style=for-the-badge 182 | [forks-url]: https://github.com/GRVYDEV/repo/network/members 183 | [stars-shield]: https://img.shields.io/github/stars/GRVYDEV/repo.svg?style=for-the-badge 184 | [stars-url]: https://github.com/GRVYDEV/repo/stargazers 185 | [issues-shield]: https://img.shields.io/github/issues/GRVYDEV/repo.svg?style=for-the-badge 186 | [issues-url]: https://github.com/GRVYDEV/repo/issues 187 | [license-shield]: https://img.shields.io/github/license/GRVYDEV/repo.svg?style=for-the-badge 188 | [license-url]: https://github.com/GRVYDEV/repo/blob/master/LICENSE.txt 189 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 190 | [linkedin-url]: https://linkedin.com/in/GRVYDEV 191 | -------------------------------------------------------------------------------- /webrtc/main.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | 17 | "github.com/GRVYDEV/lightspeed-webrtc/ws" 18 | "github.com/gorilla/websocket" 19 | 20 | "github.com/pion/interceptor" 21 | "github.com/pion/rtp" 22 | "github.com/pion/webrtc/v3" 23 | ) 24 | 25 | var ( 26 | addr = flag.String("addr", "localhost", "http service address") 27 | ip = flag.String("ip", "none", "IP address for webrtc") 28 | wsPort = flag.Int("ws-port", 8080, "Port for websocket") 29 | rtpPort = flag.Int("rtp-port", 65535, "Port for RTP") 30 | ports = flag.String("ports", "20000-20500", "Port range for webrtc") 31 | iceSrv = flag.String("ice-servers", "none", "Comma seperated list of ICE / STUN servers (optional)") 32 | sslCert = flag.String("ssl-cert", "", "Ssl cert for websocket (optional)") 33 | sslKey = flag.String("ssl-key", "", "Ssl key for websocket (optional)") 34 | upgrader = websocket.Upgrader{ 35 | CheckOrigin: func(r *http.Request) bool { return true }, 36 | } 37 | 38 | videoTrack *webrtc.TrackLocalStaticRTP 39 | 40 | audioTrack *webrtc.TrackLocalStaticRTP 41 | 42 | hub *ws.Hub 43 | ) 44 | 45 | func main() { 46 | flag.Parse() 47 | log.SetFlags(0) 48 | 49 | // Open a UDP Listener for RTP Packets on port 65535 50 | listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP(*addr), Port: *rtpPort}) 51 | if err != nil { 52 | panic(err) 53 | } 54 | defer func() { 55 | if err = listener.Close(); err != nil { 56 | panic(err) 57 | } 58 | }() 59 | 60 | fmt.Println("Waiting for RTP Packets") 61 | 62 | // Create a video track 63 | videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/H264"}, "video", "pion") 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | // Create an audio track 69 | audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, "audio", "pion") 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | hub = ws.NewHub() 75 | go hub.Run() 76 | 77 | // start HTTP server 78 | go func() { 79 | http.HandleFunc("/websocket", websocketHandler) 80 | 81 | wsAddr := *addr + ":" + strconv.Itoa(*wsPort) 82 | if *sslCert != "" && *sslKey != "" { 83 | log.Fatal(http.ListenAndServeTLS(wsAddr, *sslCert, *sslKey, nil)) 84 | } else { 85 | log.Fatal(http.ListenAndServe(wsAddr, nil)) 86 | } 87 | }() 88 | 89 | inboundRTPPacket := make([]byte, 4096) // UDP MTU 90 | 91 | var once sync.Once 92 | 93 | // Read RTP packets forever and send them to the WebRTC Client 94 | for { 95 | 96 | n, _, err := listener.ReadFrom(inboundRTPPacket) 97 | 98 | once.Do(func() { fmt.Print("houston we have a packet") }) 99 | 100 | if err != nil { 101 | fmt.Printf("error during read: %s", err) 102 | panic(err) 103 | } 104 | 105 | packet := &rtp.Packet{} 106 | if err = packet.Unmarshal(inboundRTPPacket[:n]); err != nil { 107 | //It has been found that the windows version of OBS sends us some malformed packets 108 | //It does not effect the stream so we will disable any output here 109 | //fmt.Printf("Error unmarshaling RTP packet %s\n", err) 110 | 111 | } 112 | 113 | if packet.Header.PayloadType == 96 { 114 | if _, writeErr := videoTrack.Write(inboundRTPPacket[:n]); writeErr != nil { 115 | panic(writeErr) 116 | } 117 | } else if packet.Header.PayloadType == 97 { 118 | if _, writeErr := audioTrack.Write(inboundRTPPacket[:n]); writeErr != nil { 119 | panic(writeErr) 120 | } 121 | } 122 | 123 | } 124 | 125 | } 126 | 127 | // Create a new webrtc.API object that takes public IP addresses and port ranges into account. 128 | func createWebrtcApi() *webrtc.API { 129 | s := webrtc.SettingEngine{} 130 | 131 | // Set a NAT IP if one is given -- only if no ICE servers are provided 132 | if *ip != "none" && *iceSrv == "none" { 133 | s.SetNAT1To1IPs([]string{*ip}, webrtc.ICECandidateTypeHost) 134 | } 135 | 136 | // Split given port range into two sides, pass them to SettingEngine 137 | pr := strings.SplitN(*ports, "-", 2) 138 | 139 | pr_low, err := strconv.ParseUint(pr[0], 10, 16) 140 | if err != nil { 141 | panic(err) 142 | } 143 | pr_high, err := strconv.ParseUint(pr[1], 10, 16) 144 | if err != nil { 145 | panic(err) 146 | } 147 | 148 | s.SetEphemeralUDPPortRange(uint16(pr_low), uint16(pr_high)) 149 | 150 | // Default parameters as specified in Pion's non-API NewPeerConnection call 151 | // These are needed because CreateOffer will not function without them 152 | m := &webrtc.MediaEngine{} 153 | if err := m.RegisterDefaultCodecs(); err != nil { 154 | panic(err) 155 | } 156 | 157 | i := &interceptor.Registry{} 158 | if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { 159 | panic(err) 160 | } 161 | 162 | return webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i), webrtc.WithSettingEngine(s)) 163 | } 164 | 165 | // Handle incoming websockets 166 | func websocketHandler(w http.ResponseWriter, r *http.Request) { 167 | 168 | // Upgrade HTTP request to Websocket 169 | conn, err := upgrader.Upgrade(w, r, nil) 170 | if err != nil { 171 | log.Print("upgrade:", err) 172 | return 173 | } 174 | 175 | // When this frame returns close the Websocket 176 | defer conn.Close() //nolint 177 | 178 | // Create API that takes IP and port range into account 179 | api := createWebrtcApi() 180 | 181 | // Create the WebRTC config with ICE server configuration 182 | var webrtcCfg webrtc.Configuration 183 | if *iceSrv != "none" { 184 | iceUrls := strings.Split(*iceSrv, ",") 185 | iceServers := make([]webrtc.ICEServer, len(iceUrls)) 186 | for idx, url := range iceUrls { 187 | iceServers[idx] = webrtc.ICEServer{ 188 | URLs: []string{url}, 189 | } 190 | } 191 | webrtcCfg = webrtc.Configuration{ 192 | ICEServers: iceServers, 193 | } 194 | } else { 195 | webrtcCfg = webrtc.Configuration{} 196 | } 197 | 198 | // Create new PeerConnection 199 | peerConnection, err := api.NewPeerConnection(webrtcCfg) 200 | if err != nil { 201 | log.Print(err) 202 | return 203 | } 204 | 205 | // When this frame returns close the PeerConnection 206 | defer peerConnection.Close() //nolint 207 | 208 | // Accept one audio and one video track Outgoing 209 | transceiverVideo, err := peerConnection.AddTransceiverFromTrack(videoTrack, 210 | webrtc.RTPTransceiverInit{ 211 | Direction: webrtc.RTPTransceiverDirectionSendonly, 212 | }, 213 | ) 214 | transceiverAudio, err := peerConnection.AddTransceiverFromTrack(audioTrack, 215 | webrtc.RTPTransceiverInit{ 216 | Direction: webrtc.RTPTransceiverDirectionSendonly, 217 | }, 218 | ) 219 | if err != nil { 220 | log.Print(err) 221 | return 222 | } 223 | go func() { 224 | rtcpBuf := make([]byte, 1500) 225 | for { 226 | if _, _, rtcpErr := transceiverVideo.Sender().Read(rtcpBuf); rtcpErr != nil { 227 | return 228 | } 229 | if _, _, rtcpErr := transceiverAudio.Sender().Read(rtcpBuf); rtcpErr != nil { 230 | return 231 | } 232 | } 233 | }() 234 | 235 | c := ws.NewClient(hub, conn, peerConnection) 236 | 237 | go c.WriteLoop() 238 | 239 | // Add to the hub 240 | hub.Register <- c 241 | 242 | // Trickle ICE. Emit server candidate to client 243 | peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { 244 | if i == nil { 245 | return 246 | } 247 | 248 | candidateString, err := json.Marshal(i.ToJSON()) 249 | if err != nil { 250 | log.Println(err) 251 | return 252 | } 253 | 254 | if msg, err := json.Marshal(ws.WebsocketMessage{ 255 | Event: ws.MessageTypeCandidate, 256 | Data: candidateString, 257 | }); err == nil { 258 | hub.RLock() 259 | if _, ok := hub.Clients[c]; ok { 260 | c.Send <- msg 261 | } 262 | hub.RUnlock() 263 | } else { 264 | log.Println(err) 265 | } 266 | }) 267 | 268 | // If PeerConnection is closed remove it from global list 269 | peerConnection.OnConnectionStateChange(func(p webrtc.PeerConnectionState) { 270 | switch p { 271 | case webrtc.PeerConnectionStateFailed: 272 | if err := peerConnection.Close(); err != nil { 273 | log.Print(err) 274 | } 275 | hub.Unregister <- c 276 | 277 | case webrtc.PeerConnectionStateClosed: 278 | hub.Unregister <- c 279 | } 280 | }) 281 | 282 | offer, err := peerConnection.CreateOffer(nil) 283 | if err != nil { 284 | log.Print(err) 285 | } 286 | 287 | if err = peerConnection.SetLocalDescription(offer); err != nil { 288 | log.Print(err) 289 | } 290 | 291 | offerString, err := json.Marshal(offer) 292 | if err != nil { 293 | log.Print(err) 294 | } 295 | 296 | if msg, err := json.Marshal(ws.WebsocketMessage{ 297 | Event: ws.MessageTypeOffer, 298 | Data: offerString, 299 | }); err == nil { 300 | hub.RLock() 301 | if _, ok := hub.Clients[c]; ok { 302 | c.Send <- msg 303 | } 304 | hub.RUnlock() 305 | } else { 306 | log.Printf("could not marshal ws message: %s", err) 307 | } 308 | 309 | c.ReadLoop() 310 | } 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 |

6 |

Project Lightspeed

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

17 | A self contained OBS -> FTL -> WebRTC live streaming server. Comprised of 3 parts once configured anyone can achieve sub-second OBS to the browser livestreaming 18 | 19 | 20 |
21 |
22 | View Demo 23 | · 24 | Report a Bug 25 | · 26 | Request Features 27 |

28 | 29 | 30 |
31 |

Table of Contents

32 |
    33 |
  1. 34 | About The Project 35 | 43 |
  2. 44 |
  3. Discord
  4. 45 |
  5. 46 | Getting Started 47 | 59 |
  6. 60 |
  7. Usage
  8. 61 |
  9. Streaming From OBS 62 | 65 |
  10. 66 |
  11. Help
  12. 67 |
  13. Roadmap
  14. 68 |
  15. Bugs
  16. 69 |
  17. Contributing
  18. 70 |
  19. License
  20. 71 |
  21. Contact
  22. 72 |
  23. Acknowledgements
  24. 73 |
74 |
75 | 76 | 77 | 78 | ## About The Project 79 | 80 | 81 | 82 | Project Lightspeed is a fully self-contained live streaming server. With Lightspeed you will be able to deploy your 83 | own sub-second latency live streaming platform. The Lightspeed repository contains the instructions for installing 84 | and deploying the entire application. So far, Lightspeed includes an ingest service, broadcast service via webRTC 85 | and a web application for viewing. Lightspeed is however completely modular. What this means is that you can write 86 | your own web app, ingest server or broadcast server. 87 | 88 | ### How It Works 89 | 90 | Lightspeed Ingest listens on port 8084 which is the port used by the FTL protocol. Upon receiving a connection it completes the FTL handshake and negotiates a port (this is currently bugged however and defaults to 65535). Once the negotiation is done Lightspeed WebRTC listens on the negotiated port (in the future Lightspeed WebRTC will listen on the loopback interface so the ingest has more control on what packets we accept) and relays the incoming RTP packets over WebRTC. Lightspeed React communicates via websocket with Lightspeed WebRTC to exchange ICE Candidates and once a connection is established the video can be viewed. 91 | 92 | ### Diagram 93 | Here is a diagram that outlines the current implementation and the future implementation that I would like to achieve. The reason I want the packets relayed from Ingest to WebRTC on the loopback interface is so that we have more control over who can send packets. Meaning that when a DISCONNECT command is recieved we can terminate the UDP listener so that someone could not start sending packets that we do not want 94 | 95 | Lightspeed Diagram 96 | 97 | ### Built With 98 | 99 | - Rust 100 | - Golang 101 | - React 102 | 103 | ### Components 104 | 105 | - [Lightspeed Ingest](https://github.com/GRVYDEV/Lightspeed-ingest) 106 | - [Lightspeed WebRTC](https://github.com/GRVYDEV/Lightspeed-webrtc) 107 | - [Lightspeed React](https://github.com/GRVYDEV/Lightspeed-react) 108 | 109 | ## Discord 110 | We now have a [Discord](https://discord.gg/UpQZANPYmZ) server! This is a great way to stay up to date with the project and join in on the conversation! Come stop by! 111 | 112 | 113 | 114 | ## Getting Started 115 | 116 | In order to get a copy running you will need to install all 3 repositories. There are installation instructions in 117 | each repo however I will include them here for the sake of simplicity. 118 | 119 | ### Prerequisites 120 | 121 | In order to run Lightspeed, [Golang](https://golang.org/doc/install), [Rust](https://www.rust-lang.org/tools/install), and [npm](https://www.npmjs.com/get-npm) are required. Additionally the Rust repo requires a C compiler. If you get a `linker cc not found` error then you need to install a C compiler. 122 | 123 | ### Installation 124 | 125 | #### Clone Repository 126 | 127 | ```sh 128 | git clone https://github.com/GRVYDEV/Project-Lightspeed 129 | ``` 130 | 131 | #### Build Ingest Server 132 | 133 | ```sh 134 | cd ingest 135 | cargo build 136 | ``` 137 | 138 | #### Build WebRTC Server 139 | 140 | Using go get 141 | 142 | **Warning: Deprecated method (relies on outdated repository)** 143 | 144 | ```sh 145 | export GO111MODULE=on 146 | go get github.com/GRVYDEV/lightspeed-webrtc 147 | ``` 148 | 149 | Using git 150 | 151 | ```sh 152 | cd webrtc 153 | export GO111MODULE=on 154 | go build 155 | ``` 156 | 157 | #### Frontend (Based on React.JS) 158 | 159 | ```sh 160 | cd frontend 161 | npm install 162 | ``` 163 | 164 | ### Community Installation (**Warning: Outdated. Uses deprecated repositories**) 165 | Some of our awesome community members have written their own installers for Lightspeed. Here are links to those! 166 | 167 | **Note**: If you want to make a custom installer do so in the `/contrib` folder and submit a PR. Please make sure to include a README on how to use it! 168 | 169 | - [Ubuntu Installer](https://github.com/GRVYDEV/Project-Lightspeed/tree/main/contrib/ubuntu_installer) 170 | 171 | 172 | 173 | ## Usage 174 | 175 | #### Lightspeed Ingest 176 | 177 | ```sh 178 | cd ingest 179 | cargo run --release 180 | ``` 181 | 182 | #### Lightspeed WebRTC 183 | 184 | Using go get 185 | 186 | **Warning: Deprecated method (relies on outdated repository)** 187 | 188 | ```sh 189 | lightspeed-webrtc --addr=XXX.XXX.XXX.XXX 190 | ``` 191 | 192 | Using git 193 | 194 | ```sh 195 | cd webrtc 196 | go build 197 | ./lightspeed-webrtc --addr=XXX.XXX.XXX.XXX 198 | ``` 199 | 200 | ##### Arguments 201 | 202 | | Argument | Supported Values | Defaults | Notes | 203 | | :-------- | :--------------- | :------- | :---------------- | 204 | | `--addr` | A valid IP address | `localhost` | This is the local Ip address of your machine. It defaults to localhost but should be set to your local IP. For example 10.17.0.5 This is where the server will listen for UDP packets and where it will host the websocket endpoint for SDP negotiation| 205 | | `--ip` | A valid IP address | `none` | Sets the public IP address for WebRTC to use. This is especially useful in the context of Docker| 206 | | `--ports` | A valid UDP port range | `20000-20500` | This sets the UDP ports that WebRTC will use to connect with the client | 207 | | `--ws-port` | A valid port number | `8080` | This is the port on which the websocket will be hosted. If you change this value make sure that is reflected in the URL used by the react client | 208 | | `--rtp-port` | A valid port number | `65535` | This is the port on which the WebRTC service will listen for RTP packets. Ensure this is the same port that Lightspeed Ingest is negotiating with the client | 209 | | `--ice-servers` | A comma separated list of hosts | `none` | List of ICE / STUN servers used by WebRTC for setting up the network connection with the clients | 210 | 211 | #### Lightspeed React 212 | 213 | You should then configure the websocket URL in `config.json` in the `build` directory. If you are using an IP then it will be the 214 | public IP of your machine if you have DNS then it will be your hostname. 215 | 216 | **Note**: The websocket port is hardcoded meaning that Lightspeed-webrtc will always serve it on port 8080 (this may change in the future) 217 | so for the websocket config it needs to be `ws://IP_or_Hostname:8080/websocket` 218 | 219 | You can host the static site locally using `serve` which can be found [here](https://www.npmjs.com/package/serve) 220 | 221 | **Note**: your version of `serve` may require the `-p` flag instead of `-l` for the port 222 | ```sh 223 | cd frontend 224 | npm run build 225 | serve -s build -l 80 226 | ``` 227 | 228 | The above will serve the build folder on port 80. 229 | 230 | View Lightspeed in your web browser by visiting http://hostname or http://your.ip.address.here 231 | 232 | 233 | --- 234 | 235 | 236 | ## Docker 237 | 238 | Install [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/). 239 | 240 | See the `.env` file to configure per your needs. At minimum, you need to set `WEBSOCKET_HOST`. The stream key will be 241 | generated automatically on boot, and change each restart, unless you set a static one. 242 | 243 | ### Development 244 | 245 | Use `docker-compose up` to start all containers at once and monitor the logs. When you are happy it is working you can 246 | move to running detached. 247 | 248 | ### Run Detached (background) 249 | 250 | Use `docker-compose up -d` to start all containers detached to have them run in the background. 251 | 252 | Use `docker ps` to verify uptime, port forwarding, etc. 253 | 254 | You can also use `docker-compose logs -f` to follow the logs of all the containers, and press `CTRL` + `C` to stop 255 | following but leave the containers running. 256 | 257 | ### Build Images manually 258 | 259 | For development purposes you can choose to build the containers locally instead of Docker Hub. Uncomment the `build:` and `context:` directives 260 | in the docker-compose.yml 261 | 262 | ``` 263 | git clone https://github.com/GRVYDEV/Project-Lightspeed 264 | --- 265 | ./Project-Lightspeed # Monorepo Root 266 | frontend/ 267 | ingest/ 268 | webrtc/ 269 | ``` 270 | 271 | Run `docker-compose build` to build the local container images. If you change the source code you will need to run again. 272 | You can run rebuild an individual container via `docker-compose build lightspeed-webrtc`. 273 | 274 | --- 275 | 276 | ## Streaming From OBS 277 | 278 | By default, since we are using the FTL protocol, you cannot just use a Custom server. You will need to edit 279 | your `services.json` file. It can be found at: 280 | - Windows: `%AppData%\obs-studio\plugin_config\rtmp-services\services.json` 281 | - OSX: `/Users/YOURUSERNAME/Library/Application\ Support/obs-studio/plugin_config/rtmp-services/services.json` 282 | 283 | **Note**: Not all versions of Linux have access to OBS with the FTL SDK built in. If you are on Linux and you cannot stream to Lightspeed this may be the issue. 284 | 285 | Paste the below into the services array and change the url to either the IP or the hostname of your Project Lightspeed server 286 | 287 | **Note**: for the url it is not prefaced by anything. For example, given an IP of 10.0.0.2 you would put `"url": "10.0.0.2"` You do not need to indicate a port since the FTL protocol always uses 8084 288 | ```json 289 | { 290 | "name": "Project Lightspeed", 291 | "common": false, 292 | "servers": [ 293 | { 294 | "name": "SERVER TITLE HERE", 295 | "url": "your.lightspeed.hostname" 296 | } 297 | ], 298 | "recommended": { 299 | "keyint": 2, 300 | "output": "ftl_output", 301 | "max audio bitrate": 160, 302 | "max video bitrate": 8000, 303 | "profile": "main", 304 | "bframes": 0 305 | } 306 | }, 307 | ``` 308 | 309 | NOTE: You do not need to specify a port. 310 | 311 | After restarting OBS you should be able to see your service in the OBS settings Stream pane. 312 | (Special Thanks to [Glimesh](https://github.com/Glimesh) for these instructions) 313 | 314 | --- 315 | 316 | ### Stream Key 317 | 318 | We are no longer using a default streamkey! If you are still using one please pull from master on the Lightspeed-ingest 319 | repository. Now, by default on first time startup a new streamkey will be generated and output to the terminal for you. 320 | In order to regenerate this key simply delete the file it generates called `hash`. In a Docker context we will work to 321 | make the key reset process as easy as possible. Simply copy the key output in the terminal to OBS and you are all set! 322 | This key WILL NOT change unless the `hash` file is deleted. 323 | 324 | Streamkey example 325 | 326 | ## Help 327 | This project is still very much a work in progress and a lot of improvements will be made to the deployment process. 328 | If something is unclear or you are stuck there are two main ways you can get help. 329 | 330 | 1. [Discord](https://discord.gg/UpQZANPYmZ) - this is the quickest and easiest way I will be able to help you through some deployment issues. 331 | 2. [Create an Issue](https://github.com/GRVYDEV/Project-Lightspeed/issues) - this is another way you can bring attention to something that you want fixed. 332 | 333 | 334 | 335 | ## Roadmap 336 | 337 | I will be fleshing out the roadmap in the coming days. As of right now I want to get this to a point where it is 338 | as close to other live streaming services as possible. If there are any features that you want to see then feel 339 | free to suggest them! 340 | 341 | See the [open issues](https://github.com/GRVYDEV/Project-Lightspeed/issues) for a list of proposed features 342 | (and known issues). 343 | 344 | ## Bugs 345 | 346 | I am very from perfect and there are bound to be bugs and things I've overlooked in the installation process. 347 | Please, add issues and feel free to reach out if anything is unclear. Also, we have a Discord. 348 | 349 | 350 | 351 | ## Contributing 352 | 353 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. 354 | Any contributions you make are **greatly appreciated**. 355 | 356 | 1. Fork the Project 357 | 2. Create your Feature Branch: ``git checkout -b feature/AmazingFeature`` 358 | 3. Commit your Changes: ``git commit -m 'Add some AmazingFeature'`` 359 | 4. Push to the Branch: ``git push origin feature/AmazingFeature`` 360 | 5. Open a Pull Request 361 | 362 | 363 | 364 | ## License 365 | 366 | Distributed under the MIT License. See `LICENSE` for more information. 367 | 368 | 369 | 370 | ## Contact 371 | 372 | Garrett Graves - [@grvydev](https://twitter.com/grvydev) 373 | 374 | Project Link: [https://github.com/GRVYDEV/Project-Lightspeed](https://github.com/GRVYDEV/Project-Lightspeed) 375 | 376 | 377 | 378 | ## Acknowledgements 379 | 380 | - [Sean Dubois](https://github.com/Sean-Der) 381 | - [Hayden McAfee](https://github.com/haydenmc) 382 | 383 | -------------------------------------------------------------------------------- /ingest/src/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::ftl_codec::{FtlCodec, FtlCommand}; 2 | use futures::{SinkExt, StreamExt}; 3 | use hex::{decode, encode}; 4 | use log::{error, info, warn}; 5 | use rand::distributions::{Alphanumeric, Uniform}; 6 | use rand::{thread_rng, Rng}; 7 | use ring::hmac; 8 | use std::fs; 9 | use tokio::net::TcpStream; 10 | use tokio::sync::mpsc; 11 | use tokio_util::codec::Framed; 12 | 13 | #[derive(Debug)] 14 | enum FrameCommand { 15 | Send { data: Vec }, 16 | // Kill, 17 | } 18 | pub struct Connection {} 19 | #[derive(Debug)] 20 | pub struct ConnectionState { 21 | pub hmac_payload: Option, 22 | pub protocol_version: Option, 23 | pub vendor_name: Option, 24 | pub vendor_version: Option, 25 | pub video: bool, 26 | pub video_codec: Option, 27 | pub video_height: Option, 28 | pub video_width: Option, 29 | pub video_payload_type: Option, 30 | pub video_ingest_ssrc: Option, 31 | pub audio: bool, 32 | pub audio_codec: Option, 33 | pub audio_payload_type: Option, 34 | pub audio_ingest_ssrc: Option, 35 | } 36 | 37 | impl ConnectionState { 38 | pub fn get_payload(&self) -> String { 39 | match &self.hmac_payload { 40 | Some(payload) => payload.clone(), 41 | None => String::new(), 42 | } 43 | } 44 | pub fn new() -> ConnectionState { 45 | ConnectionState { 46 | hmac_payload: None, 47 | protocol_version: None, 48 | vendor_name: None, 49 | vendor_version: None, 50 | video: false, 51 | video_codec: None, 52 | video_height: None, 53 | video_width: None, 54 | video_payload_type: None, 55 | video_ingest_ssrc: None, 56 | audio: false, 57 | audio_codec: None, 58 | audio_ingest_ssrc: None, 59 | audio_payload_type: None, 60 | } 61 | } 62 | pub fn print(&self) { 63 | match &self.protocol_version { 64 | Some(p) => info!("Protocol Version: {}", p), 65 | None => warn!("Protocol Version: None"), 66 | } 67 | match &self.vendor_name { 68 | Some(v) => info!("Vendor Name: {}", v), 69 | None => warn!("Vendor Name: None"), 70 | } 71 | match &self.vendor_version { 72 | Some(v) => info!("Vendor Version: {}", v), 73 | None => warn!("Vendor Version: None"), 74 | } 75 | match &self.video_codec { 76 | Some(v) => info!("Video Codec: {}", v), 77 | None => warn!("Video Codec: None"), 78 | } 79 | 80 | match &self.video_height { 81 | Some(v) => info!("Video Height: {}", v), 82 | None => warn!("Video Height: None"), 83 | } 84 | match &self.video_width { 85 | Some(v) => info!("Video Width: {}", v), 86 | None => warn!("Video Width: None"), 87 | } 88 | match &self.audio_codec { 89 | Some(a) => info!("Audio Codec: {}", a), 90 | None => warn!("Audio Codec: None"), 91 | } 92 | } 93 | } 94 | impl Connection { 95 | //initialize connection 96 | pub fn init(stream: TcpStream) { 97 | //Initialize 2 channels so we can communicate between the frame task and the command handling task 98 | let (frame_send, mut conn_receive) = mpsc::channel::(2); 99 | let (conn_send, mut frame_receive) = mpsc::channel::(2); 100 | //spawn a task whos sole job is to interact with the frame to send and receive information through the codec 101 | tokio::spawn(async move { 102 | let mut frame = Framed::new(stream, FtlCodec::new()); 103 | loop { 104 | //wait until there is a command present 105 | match frame.next().await { 106 | Some(Ok(command)) => { 107 | //send the command to the command handling task 108 | match frame_send.send(command).await { 109 | Ok(_) => { 110 | //wait for the command handling task to send us instructions 111 | let command = frame_receive.recv().await; 112 | //handle the instructions that we received 113 | match handle_frame_command(command, &mut frame).await { 114 | Ok(_) => {} 115 | Err(e) => { 116 | error!("There was an error handing frame command {:?}", e); 117 | return; 118 | } 119 | }; 120 | } 121 | Err(e) => { 122 | error!( 123 | "There was an error sending the command to the connection Error: {:?}", e 124 | ); 125 | return; 126 | } 127 | }; 128 | } 129 | Some(Err(e)) => { 130 | error!("There was an error {:?}", e); 131 | return; 132 | } 133 | None => { 134 | error!("There was a socket reading error"); 135 | return; 136 | } 137 | }; 138 | } 139 | }); 140 | 141 | tokio::spawn(async move { 142 | //initialize new connection state 143 | let mut state = ConnectionState::new(); 144 | loop { 145 | //wait until the frame task sends us a command 146 | match conn_receive.recv().await { 147 | Some(FtlCommand::Disconnect) => { 148 | //TODO: Determine what needs to happen here 149 | } 150 | //this command is where we tell the client what port to use 151 | //WARNING: This command does not work properly. 152 | //For some reason the client does not like the port we are sending and defaults to 65535 this is fine for now but will be fixed in the future 153 | Some(FtlCommand::Dot) => { 154 | let resp_string = "200 hi. Use UDP port 65535\n".to_string(); 155 | let mut resp = Vec::new(); 156 | resp.push(resp_string); 157 | //tell the frame task to send our response 158 | match conn_send.send(FrameCommand::Send { data: resp }).await { 159 | Ok(_) => { 160 | info!("Client connected!"); 161 | state.print() 162 | } 163 | Err(e) => { 164 | error!("Error sending to frame task (From: Handle HMAC) {:?}", e); 165 | return; 166 | } 167 | } 168 | } 169 | Some(command) => { 170 | handle_command(command, &conn_send, &mut state).await; 171 | } 172 | None => { 173 | error!("Nothing received from the frame"); 174 | return; 175 | } 176 | } 177 | } 178 | }); 179 | } 180 | } 181 | 182 | async fn handle_frame_command( 183 | command: Option, 184 | frame: &mut Framed, 185 | ) -> Result<(), String> { 186 | match command { 187 | Some(FrameCommand::Send { data }) => { 188 | let mut d: Vec = data.clone(); 189 | d.reverse(); 190 | while !d.is_empty() { 191 | let item = d.pop().unwrap(); 192 | match frame.send(item.clone()).await { 193 | Ok(_) => {} 194 | Err(e) => { 195 | info!("There was an error {:?}", e); 196 | return Err(format!("There was an error {:?}", e)); 197 | } 198 | } 199 | } 200 | 201 | return Ok(()); 202 | } 203 | // Some(FrameCommand::Kill) => { 204 | // info!("TODO: Implement Kill command"); 205 | // return Ok(()); 206 | // } 207 | None => { 208 | info!("Error receiving command from conn"); 209 | return Err("Error receiving command from conn".to_string()); 210 | } 211 | }; 212 | } 213 | 214 | async fn handle_command( 215 | command: FtlCommand, 216 | sender: &mpsc::Sender, 217 | conn: &mut ConnectionState, 218 | ) { 219 | match command { 220 | FtlCommand::HMAC => { 221 | conn.hmac_payload = Some(generate_hmac()); 222 | let resp = vec!["200 ".to_string(), conn.get_payload(), "\n".to_string()]; 223 | match sender.send(FrameCommand::Send { data: resp }).await { 224 | Ok(_) => {} 225 | Err(e) => { 226 | error!("Error sending to frame task (From: Handle HMAC) {:?}", e); 227 | } 228 | } 229 | } 230 | FtlCommand::Connect { data } => { 231 | //make sure we receive a valid channel id and stream key 232 | match (data.get("stream_key"), data.get("channel_id")) { 233 | (Some(key), Some(_channel_id)) => { 234 | //decode the client hash 235 | let client_hash = hex::decode(key).expect("error with hash decode"); 236 | let key = hmac::Key::new(hmac::HMAC_SHA512, &read_stream_key(false, Some(""))); 237 | //compare the two hashes to ensure they match 238 | match hmac::verify( 239 | &key, 240 | decode(conn.get_payload().into_bytes()) 241 | .expect("error with payload decode") 242 | .as_slice(), 243 | client_hash.as_slice(), 244 | ) { 245 | Ok(_) => { 246 | info!("Hashes match!"); 247 | let resp = vec!["200\n".to_string()]; 248 | match sender.send(FrameCommand::Send { data: resp }).await { 249 | Ok(_) => {} 250 | Err(e) => error!( 251 | "Error sending to frame task (From: Handle Connection) {:?}", 252 | e 253 | ), 254 | } 255 | } 256 | _ => { 257 | error!("Hashes do not equal"); 258 | } 259 | }; 260 | } 261 | 262 | (None, _) => { 263 | error!("No stream key attached to connect command"); 264 | } 265 | (_, None) => { 266 | error!("No channel id attached to connect command"); 267 | } 268 | } 269 | } 270 | FtlCommand::Attribute { data } => { 271 | match (data.get("key"), data.get("value")) { 272 | (Some(key), Some(value)) => { 273 | // info!("Key: {:?}, value: {:?}", key, value); 274 | match key.as_str() { 275 | "ProtocolVersion" => conn.protocol_version = Some(value.to_string()), 276 | "VendorName" => conn.vendor_name = Some(value.to_string()), 277 | "VendorVersion" => conn.vendor_version = Some(value.to_string()), 278 | "Video" => { 279 | match value.as_str() { 280 | "true" => conn.video = true, 281 | "false" => conn.video = false, 282 | _ => { 283 | error!("Invalid video value! Atrribute parse failed. Value was: {:?}", value); 284 | return; 285 | } 286 | } 287 | } 288 | "VideoCodec" => conn.video_codec = Some(value.to_string()), 289 | "VideoHeight" => conn.video_height = Some(value.to_string()), 290 | "VideoWidth" => conn.video_width = Some(value.to_string()), 291 | "VideoPayloadType" => conn.video_payload_type = Some(value.to_string()), 292 | "VideoIngestSSRC" => conn.video_ingest_ssrc = Some(value.to_string()), 293 | "Audio" => { 294 | match value.as_str() { 295 | "true" => conn.audio = true, 296 | "false" => conn.audio = false, 297 | _ => { 298 | error!("Invalid audio value! Atrribute parse failed. Value was: {:?}", value); 299 | return; 300 | } 301 | } 302 | } 303 | "AudioCodec" => conn.audio_codec = Some(value.to_string()), 304 | "AudioPayloadType" => conn.audio_payload_type = Some(value.to_string()), 305 | "AudioIngestSSRC" => conn.audio_ingest_ssrc = Some(value.to_string()), 306 | _ => { 307 | error!("Invalid attribute command. Attribute parsing failed. Key was {:?}, Value was {:?}", key, value) 308 | } 309 | } 310 | // No actual response is expected but if we do not respond at all the client 311 | // stops sending for some reason. 312 | let resp = vec!["".to_string()]; 313 | match sender.send(FrameCommand::Send { data: resp }).await { 314 | Ok(_) => {} 315 | Err(e) => error!( 316 | "Error sending to frame task (From: Handle Connection) {:?}", 317 | e 318 | ), 319 | } 320 | } 321 | (None, Some(_value)) => {} 322 | (Some(_key), None) => {} 323 | (None, None) => {} 324 | } 325 | } 326 | FtlCommand::Ping => { 327 | // info!("Handling PING Command"); 328 | let resp = vec!["201\n".to_string()]; 329 | match sender.send(FrameCommand::Send { data: resp }).await { 330 | Ok(_) => {} 331 | Err(e) => error!( 332 | "Error sending to frame task (From: Handle Connection) {:?}", 333 | e 334 | ), 335 | } 336 | } 337 | _ => { 338 | warn!("Command not implemented yet. Tell GRVY to quit his day job"); 339 | } 340 | } 341 | } 342 | 343 | fn generate_hmac() -> String { 344 | let dist = Uniform::new(0x00, 0xFF); 345 | let mut hmac_payload: Vec = Vec::new(); 346 | let mut rng = thread_rng(); 347 | for _ in 0..128 { 348 | hmac_payload.push(rng.sample(dist)); 349 | } 350 | encode(hmac_payload.as_slice()) 351 | } 352 | 353 | fn generate_stream_key() -> Vec { 354 | let stream_key: String = String::from_utf8(thread_rng() 355 | .sample_iter(&Alphanumeric).take(32).collect()) 356 | .expect("Failed to convert random key to string! Please open an issue and tell the devs to handle this!"); 357 | fs::write("hash", hex::encode(&stream_key)).expect("Unable to write file"); 358 | 359 | stream_key.as_bytes().to_vec() 360 | } 361 | 362 | fn print_stream_key(stream_key: Vec) { 363 | info!( 364 | // ANSI escape codes to color stream key output 365 | "Your stream key is: \x1b[31;1;4m77-{}\x1b[0m", 366 | std::str::from_utf8(&stream_key).unwrap() 367 | ); 368 | } 369 | 370 | pub fn read_stream_key(startup: bool, stream_key_env: Option<&str>) -> Vec { 371 | if startup { 372 | if let Some(stream_key) = stream_key_env { 373 | if !stream_key.is_empty() { 374 | let key = stream_key.as_bytes().to_vec(); 375 | print_stream_key(key.to_vec()); 376 | fs::write("hash", hex::encode(&stream_key)) 377 | .expect("Unable to write stream key to hash file"); 378 | return key; 379 | } 380 | } 381 | match fs::read_to_string("hash") { 382 | Err(_) => { 383 | let stream_key = generate_stream_key(); 384 | warn!("Could not read stream key. Re-generating..."); 385 | print_stream_key(stream_key.to_vec()); 386 | stream_key 387 | } 388 | Ok(file) => { 389 | info!("Loading existing stream key..."); 390 | match hex::decode(file) { 391 | Err(_) => { 392 | let stream_key = generate_stream_key(); 393 | warn!("Error decoding stream key. Re-generating..."); 394 | print_stream_key(stream_key.to_vec()); 395 | stream_key 396 | } 397 | Ok(stream_key) => { 398 | print_stream_key(stream_key.to_vec()); 399 | stream_key 400 | } 401 | } 402 | } 403 | } 404 | } else { 405 | let file = fs::read_to_string("hash").unwrap(); 406 | hex::decode(file).unwrap() 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /webrtc/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 6 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 9 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 10 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 11 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 12 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 13 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 14 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 15 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 16 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 17 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 18 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 19 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 20 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 21 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 22 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 23 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 25 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 26 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 27 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 28 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 33 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 34 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 35 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 36 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 37 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 38 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 39 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 40 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 41 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 42 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 43 | github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= 44 | github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= 45 | github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E= 46 | github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ= 47 | github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI= 48 | github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI= 49 | github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio= 50 | github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= 51 | github.com/pion/ice/v2 v2.0.14 h1:FxXxauyykf89SWAtkQCfnHkno6G8+bhRkNguSh9zU+4= 52 | github.com/pion/ice/v2 v2.0.14/go.mod h1:wqaUbOq5ObDNU5ox1hRsEst0rWfsKuH1zXjQFEWiZwM= 53 | github.com/pion/ice/v2 v2.2.3 h1:kBVhmtMcI1L3bWDepilO9kKpCGpLQeppCuVxVS8obhE= 54 | github.com/pion/ice/v2 v2.2.3/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE= 55 | github.com/pion/interceptor v0.0.8 h1:qsVJv9RF7mPq/RUnUV5iZCzxwGizO880FuiFKkEGQaE= 56 | github.com/pion/interceptor v0.0.8/go.mod h1:dHgEP5dtxOTf21MObuBAjJeAayPxLUAZjerGH8Xr07c= 57 | github.com/pion/interceptor v0.1.10 h1:DJ2GjMGm4XGIQgMJxuEpdaExdY/6RdngT7Uh4oVmquU= 58 | github.com/pion/interceptor v0.1.10/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U= 59 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 60 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 61 | github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= 62 | github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= 63 | github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= 64 | github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= 65 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 66 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 67 | github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= 68 | github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= 69 | github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= 70 | github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U= 71 | github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= 72 | github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 73 | github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U= 74 | github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 75 | github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 76 | github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 77 | github.com/pion/rtp v1.7.12 h1:Wtrx1btLYn96vQGx35UTpgRBG/MGJmIHvrGND1m219A= 78 | github.com/pion/rtp v1.7.12/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 79 | github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= 80 | github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE= 81 | github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= 82 | github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= 83 | github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA= 84 | github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= 85 | github.com/pion/sdp/v3 v3.0.3 h1:gJK9hk+JFD2NGIM1nXmqNCq1DkVaIZ9dlA3u3otnkaw= 86 | github.com/pion/sdp/v3 v3.0.3/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= 87 | github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= 88 | github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= 89 | github.com/pion/srtp/v2 v2.0.0-rc.3 h1:1fPiK1nJlNyh235tSGgBnXrPc99wK1/D707f6ntb3qY= 90 | github.com/pion/srtp/v2 v2.0.0-rc.3/go.mod h1:S6J9oY6ahAXdU3ni4nUwhWTJuBfssFjPxoB0u41TBpY= 91 | github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ= 92 | github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A= 93 | github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= 94 | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= 95 | github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= 96 | github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= 97 | github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= 98 | github.com/pion/transport v0.12.0 h1:UFmOBBZkTZ3LgvLRf/NGrfWdZEubcU6zkLU3PsA9YvU= 99 | github.com/pion/transport v0.12.0/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 100 | github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 101 | github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= 102 | github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY= 103 | github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= 104 | github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= 105 | github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= 106 | github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw= 107 | github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= 108 | github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= 109 | github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= 110 | github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= 111 | github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= 112 | github.com/pion/webrtc/v3 v3.0.0 h1:/eTiY3NbfpKj5op8cqtCZlpTv9/yumd17YRinDNOUX0= 113 | github.com/pion/webrtc/v3 v3.0.0/go.mod h1:/xwKHOAk1Y8dspJcxMwuTtxpi8t/Gzks37iB3W6hNuM= 114 | github.com/pion/webrtc/v3 v3.1.28 h1:cNUENLrHmY3PWO9na3RGrhnSjzPLQyXRVRDREC7a5Ug= 115 | github.com/pion/webrtc/v3 v3.1.28/go.mod h1:MKhUmhMsy0NZuLpZkEvTg7tcn9HBHZO39Mh2+VDj67g= 116 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 117 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 118 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 119 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 120 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 121 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 122 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 123 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 124 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 125 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 126 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 127 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 128 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 129 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 132 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 133 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= 134 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 135 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= 136 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 137 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 138 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 139 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 140 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 141 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 142 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 143 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 144 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 145 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 146 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 147 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 148 | golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8= 149 | golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 150 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 151 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 152 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 153 | golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 154 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 155 | golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc= 156 | golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 157 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 161 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 162 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 169 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 174 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 175 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= 176 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 178 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 179 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 180 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 181 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 182 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 183 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 184 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 185 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 186 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 187 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 188 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 189 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 190 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 191 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 192 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 193 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 194 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 195 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 196 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 197 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 198 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 199 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 200 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 201 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 202 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 203 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 204 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 205 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 206 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 207 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 208 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 209 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 210 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 211 | -------------------------------------------------------------------------------- /ingest/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.15" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "ansi_term" 14 | version = "0.11.0" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 17 | dependencies = [ 18 | "winapi", 19 | ] 20 | 21 | [[package]] 22 | name = "arrayref" 23 | version = "0.3.6" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 26 | 27 | [[package]] 28 | name = "arrayvec" 29 | version = "0.5.2" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 32 | 33 | [[package]] 34 | name = "atty" 35 | version = "0.2.14" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 38 | dependencies = [ 39 | "hermit-abi", 40 | "libc", 41 | "winapi", 42 | ] 43 | 44 | [[package]] 45 | name = "autocfg" 46 | version = "1.0.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 49 | 50 | [[package]] 51 | name = "base64" 52 | version = "0.13.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 55 | 56 | [[package]] 57 | name = "bitflags" 58 | version = "1.2.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 61 | 62 | [[package]] 63 | name = "blake2b_simd" 64 | version = "0.5.11" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 67 | dependencies = [ 68 | "arrayref", 69 | "arrayvec", 70 | "constant_time_eq", 71 | ] 72 | 73 | [[package]] 74 | name = "bumpalo" 75 | version = "3.4.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" 78 | 79 | [[package]] 80 | name = "bytes" 81 | version = "1.0.1" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" 84 | 85 | [[package]] 86 | name = "cc" 87 | version = "1.0.66" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "0.1.10" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "1.0.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 102 | 103 | [[package]] 104 | name = "chrono" 105 | version = "0.4.19" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 108 | dependencies = [ 109 | "libc", 110 | "num-integer", 111 | "num-traits", 112 | "time", 113 | "winapi", 114 | ] 115 | 116 | [[package]] 117 | name = "clap" 118 | version = "2.33.3" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 121 | dependencies = [ 122 | "ansi_term", 123 | "atty", 124 | "bitflags", 125 | "strsim", 126 | "textwrap", 127 | "unicode-width", 128 | "vec_map", 129 | "yaml-rust", 130 | ] 131 | 132 | [[package]] 133 | name = "constant_time_eq" 134 | version = "0.1.5" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 137 | 138 | [[package]] 139 | name = "crossbeam-utils" 140 | version = "0.8.1" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" 143 | dependencies = [ 144 | "autocfg", 145 | "cfg-if 1.0.0", 146 | "lazy_static", 147 | ] 148 | 149 | [[package]] 150 | name = "dirs" 151 | version = "2.0.2" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 154 | dependencies = [ 155 | "cfg-if 0.1.10", 156 | "dirs-sys", 157 | ] 158 | 159 | [[package]] 160 | name = "dirs-sys" 161 | version = "0.3.5" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 164 | dependencies = [ 165 | "libc", 166 | "redox_users", 167 | "winapi", 168 | ] 169 | 170 | [[package]] 171 | name = "futures" 172 | version = "0.3.9" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "c70be434c505aee38639abccb918163b63158a4b4bb791b45b7023044bdc3c9c" 175 | dependencies = [ 176 | "futures-channel", 177 | "futures-core", 178 | "futures-executor", 179 | "futures-io", 180 | "futures-sink", 181 | "futures-task", 182 | "futures-util", 183 | ] 184 | 185 | [[package]] 186 | name = "futures-channel" 187 | version = "0.3.9" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "f01c61843314e95f96cc9245702248733a3a3d744e43e2e755e3c7af8348a0a9" 190 | dependencies = [ 191 | "futures-core", 192 | "futures-sink", 193 | ] 194 | 195 | [[package]] 196 | name = "futures-core" 197 | version = "0.3.9" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "db8d3b0917ff63a2a96173133c02818fac4a746b0a57569d3baca9ec0e945e08" 200 | 201 | [[package]] 202 | name = "futures-executor" 203 | version = "0.3.9" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "9ee9ca2f7eb4475772cf39dd1cd06208dce2670ad38f4d9c7262b3e15f127068" 206 | dependencies = [ 207 | "futures-core", 208 | "futures-task", 209 | "futures-util", 210 | ] 211 | 212 | [[package]] 213 | name = "futures-io" 214 | version = "0.3.9" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "e37c1a51b037b80922864b8eed90692c5cd8abd4c71ce49b77146caa47f3253b" 217 | 218 | [[package]] 219 | name = "futures-macro" 220 | version = "0.3.9" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "0f8719ca0e1f3c5e34f3efe4570ef2c0610ca6da85ae7990d472e9cbfba13664" 223 | dependencies = [ 224 | "proc-macro-hack", 225 | "proc-macro2", 226 | "quote", 227 | "syn", 228 | ] 229 | 230 | [[package]] 231 | name = "futures-sink" 232 | version = "0.3.9" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "f6adabac1290109cfa089f79192fb6244ad2c3f1cc2281f3e1dd987592b71feb" 235 | 236 | [[package]] 237 | name = "futures-task" 238 | version = "0.3.9" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "a92a0843a2ff66823a8f7c77bffe9a09be2b64e533562c412d63075643ec0038" 241 | dependencies = [ 242 | "once_cell", 243 | ] 244 | 245 | [[package]] 246 | name = "futures-util" 247 | version = "0.3.9" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "036a2107cdeb57f6d7322f1b6c363dad67cd63ca3b7d1b925bdf75bd5d96cda9" 250 | dependencies = [ 251 | "futures-channel", 252 | "futures-core", 253 | "futures-io", 254 | "futures-macro", 255 | "futures-sink", 256 | "futures-task", 257 | "memchr", 258 | "pin-project-lite", 259 | "pin-utils", 260 | "proc-macro-hack", 261 | "proc-macro-nested", 262 | "slab", 263 | ] 264 | 265 | [[package]] 266 | name = "getrandom" 267 | version = "0.1.16" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 270 | dependencies = [ 271 | "cfg-if 1.0.0", 272 | "libc", 273 | "wasi 0.9.0+wasi-snapshot-preview1", 274 | ] 275 | 276 | [[package]] 277 | name = "getrandom" 278 | version = "0.2.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "4060f4657be78b8e766215b02b18a2e862d83745545de804638e2b545e81aee6" 281 | dependencies = [ 282 | "cfg-if 1.0.0", 283 | "libc", 284 | "wasi 0.10.1+wasi-snapshot-preview1", 285 | ] 286 | 287 | [[package]] 288 | name = "hermit-abi" 289 | version = "0.1.17" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" 292 | dependencies = [ 293 | "libc", 294 | ] 295 | 296 | [[package]] 297 | name = "hex" 298 | version = "0.4.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" 301 | 302 | [[package]] 303 | name = "instant" 304 | version = "0.1.9" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" 307 | dependencies = [ 308 | "cfg-if 1.0.0", 309 | ] 310 | 311 | [[package]] 312 | name = "js-sys" 313 | version = "0.3.46" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" 316 | dependencies = [ 317 | "wasm-bindgen", 318 | ] 319 | 320 | [[package]] 321 | name = "lazy_static" 322 | version = "1.4.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 325 | 326 | [[package]] 327 | name = "libc" 328 | version = "0.2.82" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" 331 | 332 | [[package]] 333 | name = "lightspeed-ingest" 334 | version = "0.1.0" 335 | dependencies = [ 336 | "bytes", 337 | "clap", 338 | "futures", 339 | "futures-util", 340 | "hex", 341 | "log", 342 | "rand", 343 | "regex", 344 | "ring", 345 | "rtp-rs", 346 | "simplelog", 347 | "tokio", 348 | "tokio-util", 349 | ] 350 | 351 | [[package]] 352 | name = "lock_api" 353 | version = "0.4.2" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" 356 | dependencies = [ 357 | "scopeguard", 358 | ] 359 | 360 | [[package]] 361 | name = "log" 362 | version = "0.4.13" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" 365 | dependencies = [ 366 | "cfg-if 0.1.10", 367 | ] 368 | 369 | [[package]] 370 | name = "memchr" 371 | version = "2.3.4" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 374 | 375 | [[package]] 376 | name = "mio" 377 | version = "0.7.7" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" 380 | dependencies = [ 381 | "libc", 382 | "log", 383 | "miow", 384 | "ntapi", 385 | "winapi", 386 | ] 387 | 388 | [[package]] 389 | name = "miow" 390 | version = "0.3.6" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" 393 | dependencies = [ 394 | "socket2", 395 | "winapi", 396 | ] 397 | 398 | [[package]] 399 | name = "ntapi" 400 | version = "0.3.6" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 403 | dependencies = [ 404 | "winapi", 405 | ] 406 | 407 | [[package]] 408 | name = "num-integer" 409 | version = "0.1.44" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 412 | dependencies = [ 413 | "autocfg", 414 | "num-traits", 415 | ] 416 | 417 | [[package]] 418 | name = "num-traits" 419 | version = "0.2.14" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 422 | dependencies = [ 423 | "autocfg", 424 | ] 425 | 426 | [[package]] 427 | name = "num_cpus" 428 | version = "1.13.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 431 | dependencies = [ 432 | "hermit-abi", 433 | "libc", 434 | ] 435 | 436 | [[package]] 437 | name = "once_cell" 438 | version = "1.5.2" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" 441 | 442 | [[package]] 443 | name = "parking_lot" 444 | version = "0.11.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" 447 | dependencies = [ 448 | "instant", 449 | "lock_api", 450 | "parking_lot_core", 451 | ] 452 | 453 | [[package]] 454 | name = "parking_lot_core" 455 | version = "0.8.2" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" 458 | dependencies = [ 459 | "cfg-if 1.0.0", 460 | "instant", 461 | "libc", 462 | "redox_syscall", 463 | "smallvec", 464 | "winapi", 465 | ] 466 | 467 | [[package]] 468 | name = "pin-project-lite" 469 | version = "0.2.4" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" 472 | 473 | [[package]] 474 | name = "pin-utils" 475 | version = "0.1.0" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 478 | 479 | [[package]] 480 | name = "ppv-lite86" 481 | version = "0.2.10" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 484 | 485 | [[package]] 486 | name = "proc-macro-hack" 487 | version = "0.5.19" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 490 | 491 | [[package]] 492 | name = "proc-macro-nested" 493 | version = "0.1.6" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" 496 | 497 | [[package]] 498 | name = "proc-macro2" 499 | version = "1.0.24" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 502 | dependencies = [ 503 | "unicode-xid", 504 | ] 505 | 506 | [[package]] 507 | name = "quote" 508 | version = "1.0.8" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" 511 | dependencies = [ 512 | "proc-macro2", 513 | ] 514 | 515 | [[package]] 516 | name = "rand" 517 | version = "0.8.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "c24fcd450d3fa2b592732565aa4f17a27a61c65ece4726353e000939b0edee34" 520 | dependencies = [ 521 | "libc", 522 | "rand_chacha", 523 | "rand_core", 524 | "rand_hc", 525 | ] 526 | 527 | [[package]] 528 | name = "rand_chacha" 529 | version = "0.3.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" 532 | dependencies = [ 533 | "ppv-lite86", 534 | "rand_core", 535 | ] 536 | 537 | [[package]] 538 | name = "rand_core" 539 | version = "0.6.1" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" 542 | dependencies = [ 543 | "getrandom 0.2.1", 544 | ] 545 | 546 | [[package]] 547 | name = "rand_hc" 548 | version = "0.3.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" 551 | dependencies = [ 552 | "rand_core", 553 | ] 554 | 555 | [[package]] 556 | name = "redox_syscall" 557 | version = "0.1.57" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 560 | 561 | [[package]] 562 | name = "redox_users" 563 | version = "0.3.5" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 566 | dependencies = [ 567 | "getrandom 0.1.16", 568 | "redox_syscall", 569 | "rust-argon2", 570 | ] 571 | 572 | [[package]] 573 | name = "regex" 574 | version = "1.4.3" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" 577 | dependencies = [ 578 | "aho-corasick", 579 | "memchr", 580 | "regex-syntax", 581 | "thread_local", 582 | ] 583 | 584 | [[package]] 585 | name = "regex-syntax" 586 | version = "0.6.22" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" 589 | 590 | [[package]] 591 | name = "ring" 592 | version = "0.16.19" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" 595 | dependencies = [ 596 | "cc", 597 | "libc", 598 | "once_cell", 599 | "spin", 600 | "untrusted", 601 | "web-sys", 602 | "winapi", 603 | ] 604 | 605 | [[package]] 606 | name = "rtp-rs" 607 | version = "0.5.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "e1110d695193d446e901de09921ffbf2d86ae351bbfde9c5b53863ce177e17f5" 610 | 611 | [[package]] 612 | name = "rust-argon2" 613 | version = "0.8.3" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 616 | dependencies = [ 617 | "base64", 618 | "blake2b_simd", 619 | "constant_time_eq", 620 | "crossbeam-utils", 621 | ] 622 | 623 | [[package]] 624 | name = "scopeguard" 625 | version = "1.1.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 628 | 629 | [[package]] 630 | name = "signal-hook-registry" 631 | version = "1.3.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" 634 | dependencies = [ 635 | "libc", 636 | ] 637 | 638 | [[package]] 639 | name = "simplelog" 640 | version = "0.7.6" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "3cf9a002ccce717d066b3ccdb8a28829436249867229291e91b25d99bd723f0d" 643 | dependencies = [ 644 | "chrono", 645 | "log", 646 | "term", 647 | ] 648 | 649 | [[package]] 650 | name = "slab" 651 | version = "0.4.2" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 654 | 655 | [[package]] 656 | name = "smallvec" 657 | version = "1.6.1" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 660 | 661 | [[package]] 662 | name = "socket2" 663 | version = "0.3.19" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 666 | dependencies = [ 667 | "cfg-if 1.0.0", 668 | "libc", 669 | "winapi", 670 | ] 671 | 672 | [[package]] 673 | name = "spin" 674 | version = "0.5.2" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 677 | 678 | [[package]] 679 | name = "strsim" 680 | version = "0.8.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 683 | 684 | [[package]] 685 | name = "syn" 686 | version = "1.0.58" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" 689 | dependencies = [ 690 | "proc-macro2", 691 | "quote", 692 | "unicode-xid", 693 | ] 694 | 695 | [[package]] 696 | name = "term" 697 | version = "0.6.1" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" 700 | dependencies = [ 701 | "dirs", 702 | "winapi", 703 | ] 704 | 705 | [[package]] 706 | name = "textwrap" 707 | version = "0.11.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 710 | dependencies = [ 711 | "unicode-width", 712 | ] 713 | 714 | [[package]] 715 | name = "thread_local" 716 | version = "1.1.0" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447" 719 | dependencies = [ 720 | "lazy_static", 721 | ] 722 | 723 | [[package]] 724 | name = "time" 725 | version = "0.1.43" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 728 | dependencies = [ 729 | "libc", 730 | "winapi", 731 | ] 732 | 733 | [[package]] 734 | name = "tokio" 735 | version = "1.0.1" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "d258221f566b6c803c7b4714abadc080172b272090cdc5e244a6d4dd13c3a6bd" 738 | dependencies = [ 739 | "autocfg", 740 | "bytes", 741 | "libc", 742 | "memchr", 743 | "mio", 744 | "num_cpus", 745 | "once_cell", 746 | "parking_lot", 747 | "pin-project-lite", 748 | "signal-hook-registry", 749 | "tokio-macros", 750 | "winapi", 751 | ] 752 | 753 | [[package]] 754 | name = "tokio-macros" 755 | version = "1.0.0" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "42517d2975ca3114b22a16192634e8241dc5cc1f130be194645970cc1c371494" 758 | dependencies = [ 759 | "proc-macro2", 760 | "quote", 761 | "syn", 762 | ] 763 | 764 | [[package]] 765 | name = "tokio-stream" 766 | version = "0.1.1" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "e4cdeb73537e63f98adcd73138af75e3f368ccaecffaa29d7eb61b9f5a440457" 769 | dependencies = [ 770 | "futures-core", 771 | "pin-project-lite", 772 | "tokio", 773 | ] 774 | 775 | [[package]] 776 | name = "tokio-util" 777 | version = "0.6.1" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "12ae4751faa60b9f96dd8344d74592e5a17c0c9a220413dbc6942d14139bbfcc" 780 | dependencies = [ 781 | "bytes", 782 | "futures-core", 783 | "futures-sink", 784 | "log", 785 | "pin-project-lite", 786 | "tokio", 787 | "tokio-stream", 788 | ] 789 | 790 | [[package]] 791 | name = "unicode-width" 792 | version = "0.1.8" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 795 | 796 | [[package]] 797 | name = "unicode-xid" 798 | version = "0.2.1" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 801 | 802 | [[package]] 803 | name = "untrusted" 804 | version = "0.7.1" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 807 | 808 | [[package]] 809 | name = "vec_map" 810 | version = "0.8.2" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 813 | 814 | [[package]] 815 | name = "wasi" 816 | version = "0.9.0+wasi-snapshot-preview1" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 819 | 820 | [[package]] 821 | name = "wasi" 822 | version = "0.10.1+wasi-snapshot-preview1" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9" 825 | 826 | [[package]] 827 | name = "wasm-bindgen" 828 | version = "0.2.69" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" 831 | dependencies = [ 832 | "cfg-if 1.0.0", 833 | "wasm-bindgen-macro", 834 | ] 835 | 836 | [[package]] 837 | name = "wasm-bindgen-backend" 838 | version = "0.2.69" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" 841 | dependencies = [ 842 | "bumpalo", 843 | "lazy_static", 844 | "log", 845 | "proc-macro2", 846 | "quote", 847 | "syn", 848 | "wasm-bindgen-shared", 849 | ] 850 | 851 | [[package]] 852 | name = "wasm-bindgen-macro" 853 | version = "0.2.69" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" 856 | dependencies = [ 857 | "quote", 858 | "wasm-bindgen-macro-support", 859 | ] 860 | 861 | [[package]] 862 | name = "wasm-bindgen-macro-support" 863 | version = "0.2.69" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" 866 | dependencies = [ 867 | "proc-macro2", 868 | "quote", 869 | "syn", 870 | "wasm-bindgen-backend", 871 | "wasm-bindgen-shared", 872 | ] 873 | 874 | [[package]] 875 | name = "wasm-bindgen-shared" 876 | version = "0.2.69" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" 879 | 880 | [[package]] 881 | name = "web-sys" 882 | version = "0.3.46" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" 885 | dependencies = [ 886 | "js-sys", 887 | "wasm-bindgen", 888 | ] 889 | 890 | [[package]] 891 | name = "winapi" 892 | version = "0.3.9" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 895 | dependencies = [ 896 | "winapi-i686-pc-windows-gnu", 897 | "winapi-x86_64-pc-windows-gnu", 898 | ] 899 | 900 | [[package]] 901 | name = "winapi-i686-pc-windows-gnu" 902 | version = "0.4.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 905 | 906 | [[package]] 907 | name = "winapi-x86_64-pc-windows-gnu" 908 | version = "0.4.0" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 911 | 912 | [[package]] 913 | name = "yaml-rust" 914 | version = "0.3.5" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" 917 | --------------------------------------------------------------------------------