├── 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 |
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 | You need to enable JavaScript to run this app.
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 |
4 |
5 |
6 | Project Lightspeed React
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
36 | About The Project
37 |
40 |
41 |
42 | Getting Started
43 |
47 |
48 | Usage
49 | Roadmap
50 | Contributing
51 | License
52 | Contact
53 | Acknowledgements
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 |
4 |
5 |
6 | Project Lightspeed Ingest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
36 | About The Project
37 |
40 |
41 |
42 | Getting Started
43 |
47 |
48 | Usage
49 | Streaming From OBS
50 |
53 |
54 | Roadmap
55 | Contributing
56 | License
57 | Contact
58 | Acknowledgements
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 |
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 |
4 |
5 |
6 | Project Lightspeed WebRTC
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
36 | About The Project
37 |
40 |
41 |
42 | Getting Started
43 |
47 |
48 | Usage
49 | Roadmap
50 | Contributing
51 | License
52 | Contact
53 | Acknowledgements
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 |
4 |
5 |
6 | Project Lightspeed
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
34 | About The Project
35 |
43 |
44 | Discord
45 |
46 | Getting Started
47 |
59 |
60 | Usage
61 | Streaming From OBS
62 |
65 |
66 | Help
67 | Roadmap
68 | Bugs
69 | Contributing
70 | License
71 | Contact
72 | Acknowledgements
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 |
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 |
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 |
--------------------------------------------------------------------------------