├── .dockerignore ├── .gitignore ├── .node-version ├── Dockerfile ├── README.md ├── components └── logo.js ├── fly.toml ├── package.json ├── pages ├── _app.js └── index.js ├── screenshots ├── mux-live-stream-dashboard.gif ├── wocket-live-browser-1.png └── wocket-live-browser-2.png ├── server.js ├── styles ├── demo.module.css ├── global.css └── reset.css └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .next 3 | 4 | node_modules 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.16 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:current-alpine 2 | ENV NODE_OPTIONS=--openssl-legacy-provider 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | COPY yarn.lock . 7 | 8 | RUN apk add --no-cache ffmpeg && npm install --production 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | ENV PORT=8080 15 | EXPOSE 8080 16 | CMD [ "npm", "start" ] 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Wocket](https://banner.mux.dev/?text=Wocket) 2 | 3 | # Wocket (WebSocket to RTMP) 4 | 5 | This project is a proof-of-concept to demonstrate how you can stream live from your browser to an RTMP server. Streaming via RTMP is how you stream to Twitch, Youtube Live, Facebook Live, and other live streaming platforms. Typically, this requires running a local encoder software (for example: [OBS](https://obsproject.com/) or [Ecamm Live](https://www.ecamm.com/mac/ecammlive/)). Those are great products and if you are streaming seriously you probably still want to use them. But we threw this project together to show how you might be able to pull off the same thing from a browser. In this example, instead of streaming to something like Twitch, Youtube Live, etc, we will be using the live streaming API provided by Mux, which gives you an on-demand RTMP server that you can stream to. 6 | 7 | 8 | This project uses [Next.js](https://nextjs.org) and a custom server with WebSockets. It should be noted that this project is a fun proof-of-concept. If you want to learn more about the challenges of going live from the browser take a look at this blog post [The state of going live from a browser](https://mux.com/blog/the-state-of-going-live-from-a-browser/). 9 | 10 | This is what this project looks like. This will access the browser's webcam and render it onto a canvas element. When you enter a stream key and click "Start Streaming" it will stream your webcam to a [Mux live stream](https://docs.mux.com/docs/live-streaming). 11 | 12 | ![Wocket Screenshot](./screenshots/wocket-live-browser-1.png?raw=true) 13 | ![Wocket Mobile Screenshot](./screenshots/wocket-live-browser-2.png?raw=true) 14 | ## Clone the repo 15 | 16 | ``` 17 | git clone https://github.com/MuxLabs/wocket 18 | cd wocket 19 | ``` 20 | 21 | ## Setup 22 | 23 | ### Prerequisites to run locally 24 | 25 | * To run the server locally you will need to install [ffmpeg](https://www.ffmpeg.org/) and have the command `ffmpeg` in your $PATH. To see if it is installed correctly open up a terminal and type `ffmpeg`, if you see something that is not "command not found" then you're good! 26 | 27 | For development you'll probably want to use `dev`, which will do little things like hot reloading automatically. 28 | 29 | ```javascript 30 | $ yarn install 31 | $ yarn dev 32 | ``` 33 | 34 | The last line you should see is something along the lines of: 35 | 36 | ``` 37 | $ > ready on port 3000 38 | ``` 39 | 40 | Visit that page in the browser and you should see Wocket! 41 | 42 | ### Getting a Mux stream key 43 | 44 | To get a stream key and actually start streaming to an RTMP ingest URL you will need a [free Mux account](https://dashboard.mux.com/signup?type=video). After you sign up create a live stream either [with the API](https://docs.mux.com/docs/live-streaming) or by navigating to 'Live Streams' in the dashboard and clicking 'Create New Live Stream' see below: 45 | 46 | ![Mux Dashboard Live Stream](./screenshots/mux-live-stream-dashboard.gif?raw=true). 47 | 48 | Without entering a credit card your live streams are in 'test' mode which means they are limited to 5 minutes, watermarked with the Mux logo and deleted after 24 hours. If you enter a credit card you get $20 of free credit which unlocks the full feature set and removes all limits. The $20 of credit should be plenty to cover the costs of experimenting with the API and if you need some more for experimentation please drop us a line and let us know! 49 | 50 | ## Running the application in production 51 | 52 | Again, this should just be considered a proof of concept. I didn't write this to go to production. I beg you, don't rely on this as is for something important. 53 | 54 | ``` 55 | $ yarn build 56 | $ yarn start 57 | ``` 58 | 59 | When hosting the application to an external server, it needs to use HTTPS (A non secure web site wont have access to the camera). You can use a self signed certificate for testing but on the client side you will have to trust that certificate. You can run it as 60 | ``` 61 | $ export CERT_FILE= 62 | $ export KEY_FILE= 63 | $ export SMART_TRANSCODE=true // if not set will always transcode the stream. 64 | $ export HOST=0.0.0.0 // or the IP address you want to bind to. 65 | $ npm run build 66 | $ npm start 67 | ``` 68 | 69 | ## Deploying to fly.io 70 | 71 | We will deploy the server with `flyctl`. Fly.io will use the Dockerfile to host the server. 72 | 73 | 1. Create a new fly.io app `flyctl launch` 74 | 1. When asked copying the configuration file, say "yes" 75 | 1. When asked about an app name, hit enter to get a generated name 76 | 1. When asked about which region select one or use the recommended region 77 | 1. When asked if you want a Postgresql database, say "no" 78 | 1. When asked if you want to deploy now, say "yes" 79 | 1. If it doesn't deploy properly the first time on creation and stays in the 'pending' state in the fly dashboard, try giving it a kick again by running `flyctl deploy` 80 | 81 | Read the blog post [on the fly.io blog](https://fly.io/blog/mux-fly-wocket-and-rtmp/) -- the fly.io blog is GREAT by the way, and it's full of great stuff. If it's your first time finding the fly blog, sorry for ruining the productivity of your day. 82 | 83 | ### Putting it all together 84 | 85 | The intended way of using this would be to use the [`MediaRecorder` API](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API) and send video whenever the MediaRecorder instance fires the `dataavailable` event. The [demo front-end](pages/index.js) is an example of how you could wire everything together using the `getMediaRecorder` and the `MediaRecorder` API. 86 | 87 | ## Other projects 88 | 89 | Some other projects I found when trying to figure out this whole canvas -> RTMP thing that were hugely helpful: 90 | 91 | * [fbsamples/Canvas-Streaming-Example](https://github.com/fbsamples/Canvas-Streaming-Example) 92 | * [chenxiaoqino/getusermedia-to-rtmp](https://github.com/chenxiaoqino/getusermedia-to-rtmp) 93 | 94 | Other ways of solving this problem: 95 | 96 | * [Pion](https://pion.ly/) - WebRTC implementation written in Go 97 | * [Chromium Broadcasting](https://github.com/muxinc/chromium_broadcast_demo) 98 | -------------------------------------------------------------------------------- /components/logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 | 11 | 16 | 20 | 25 | 30 | 34 | 38 | 42 | 46 | 51 | 55 | 59 | 60 | 68 | 69 | 70 | 71 | 79 | 80 | 81 | 82 | 90 | 91 | 92 | 93 | 101 | 102 | 103 | 104 | 112 | 113 | 114 | 115 | 123 | 124 | 125 | 126 | 134 | 135 | 136 | 137 | 145 | 146 | 147 | 148 | 156 | 157 | 158 | 159 | 167 | 168 | 169 | 170 | 178 | 179 | 180 | 181 | 182 | 183 | ); 184 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for polished-haze-1797 on 2022-07-11T16:50:03-07:00 2 | 3 | kill_signal = "SIGINT" 4 | kill_timeout = 5 5 | processes = [] 6 | 7 | [env] 8 | 9 | [experimental] 10 | allowed_public_ports = [] 11 | auto_rollback = true 12 | 13 | [[services]] 14 | http_checks = [] 15 | internal_port = 8080 16 | processes = ["app"] 17 | protocol = "tcp" 18 | script_checks = [] 19 | [services.concurrency] 20 | hard_limit = 25 21 | soft_limit = 20 22 | type = "connections" 23 | 24 | [[services.ports]] 25 | force_https = true 26 | handlers = ["http"] 27 | port = 80 28 | 29 | [[services.ports]] 30 | handlers = ["tls", "http"] 31 | port = 443 32 | 33 | [[services.tcp_checks]] 34 | grace_period = "1s" 35 | interval = "15s" 36 | restart_limit = 0 37 | timeout = "2s" 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-server", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": "16.x" 6 | }, 7 | "scripts": { 8 | "dev": "nodemon server.js", 9 | "build": "next build", 10 | "start": "NODE_ENV=production node server.js" 11 | }, 12 | "nodemonConfig": { 13 | "ignore": [ 14 | "pages/*" 15 | ] 16 | }, 17 | "dependencies": { 18 | "milligram": "^1.3.0", 19 | "next": "latest", 20 | "react": "latest", 21 | "react-dom": "latest", 22 | "ws": "^7.2.3" 23 | }, 24 | "devDependencies": { 25 | "nodemon": "^2.0.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | import '../styles/reset.css'; 4 | import '../styles/global.css'; 5 | 6 | function MyApp({ Component, pageProps }) { 7 | return ( 8 | <> 9 | 10 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default MyApp; 23 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import Head from 'next/head'; 3 | import styles from '../styles/demo.module.css'; 4 | 5 | const CAMERA_CONSTRAINTS = { 6 | audio: true, 7 | video: true, 8 | }; 9 | 10 | const getRecorderSettings = () => { 11 | const settings = {}; 12 | if (MediaRecorder.isTypeSupported('video/mp4')) { 13 | settings.format = 'mp4'; 14 | settings.video = 'h264'; 15 | settings.audio = 'aac'; 16 | } else { 17 | settings.format = 'webm'; 18 | settings.audio = 'opus'; 19 | settings.video = MediaRecorder.isTypeSupported('video/webm;codecs=h264') ? 'h264' : 'vp8'; 20 | } 21 | return settings; 22 | } 23 | 24 | const getRecorderMimeType = () => { 25 | const settings = getRecorderSettings(); 26 | const codecs = settings.format === 'webm' ? `;codecs="${settings.video}, ${settings.audio}"` : ''; 27 | return `video/${settings.format}${codecs}`; 28 | } 29 | 30 | export default () => { 31 | const [connected, setConnected] = useState(false); 32 | const [cameraEnabled, setCameraEnabled] = useState(false); 33 | const [streaming, setStreaming] = useState(false); 34 | const [streamKey, setStreamKey] = useState(null); 35 | const [streamUrl, setStreamUrl] = useState(null); 36 | const [textOverlay, setTextOverlay] = useState('Live from the browser!'); 37 | 38 | const inputStreamRef = useRef(); 39 | const videoRef = useRef(); 40 | const canvasRef = useRef(); 41 | const wsRef = useRef(); 42 | const mediaRecorderRef = useRef(); 43 | const requestAnimationRef = useRef(); 44 | const nameRef = useRef(); 45 | 46 | const enableCamera = async () => { 47 | inputStreamRef.current = await navigator.mediaDevices.getUserMedia( 48 | CAMERA_CONSTRAINTS 49 | ); 50 | 51 | videoRef.current.srcObject = inputStreamRef.current; 52 | 53 | await videoRef.current.play(); 54 | 55 | // We need to set the canvas height/width to match the video element. 56 | canvasRef.current.height = videoRef.current.clientHeight; 57 | canvasRef.current.width = videoRef.current.clientWidth; 58 | 59 | requestAnimationRef.current = requestAnimationFrame(updateCanvas); 60 | 61 | setCameraEnabled(true); 62 | }; 63 | 64 | const updateCanvas = () => { 65 | if (videoRef.current.ended || videoRef.current.paused) { 66 | return; 67 | } 68 | 69 | const ctx = canvasRef.current.getContext('2d'); 70 | 71 | ctx.drawImage( 72 | videoRef.current, 73 | 0, 74 | 0, 75 | videoRef.current.clientWidth, 76 | videoRef.current.clientHeight 77 | ); 78 | 79 | ctx.fillStyle = '#FB3C4E'; 80 | ctx.font = '50px Akkurat'; 81 | const date = new Date(); 82 | const dateText = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds().toString().padStart(3, '0')}`; 83 | ctx.fillText(`${nameRef.current}${dateText}`, 10, 50, canvasRef.current.width - 20); 84 | 85 | requestAnimationRef.current = requestAnimationFrame(updateCanvas); 86 | }; 87 | 88 | const stopStreaming = () => { 89 | if (mediaRecorderRef.current.state === 'recording') { 90 | mediaRecorderRef.current.stop(); 91 | } 92 | 93 | setStreaming(false); 94 | }; 95 | 96 | const startStreaming = () => { 97 | setStreaming(true); 98 | const settings = getRecorderSettings(); 99 | const protocol = window.location.protocol.replace('http', 'ws'); 100 | const wsUrl = new URL(`${protocol}//${window.location.host}/rtmp`); 101 | wsUrl.searchParams.set('video', settings.video); 102 | wsUrl.searchParams.set('audio', settings.audio); 103 | if (streamUrl) { 104 | wsUrl.searchParams.set('url', streamUrl); 105 | } 106 | if (streamKey) { 107 | wsUrl.searchParams.set('key', streamKey); 108 | } 109 | wsRef.current = new WebSocket(wsUrl); 110 | 111 | wsRef.current.addEventListener('open', function open() { 112 | setConnected(true); 113 | }); 114 | 115 | wsRef.current.addEventListener('close', () => { 116 | setConnected(false); 117 | stopStreaming(); 118 | }); 119 | 120 | const videoOutputStream = canvasRef.current.captureStream(30); // 30 FPS 121 | 122 | // Let's do some extra work to get audio to join the party. 123 | // https://hacks.mozilla.org/2016/04/record-almost-everything-in-the-browser-with-mediarecorder/ 124 | const audioStream = new MediaStream(); 125 | const audioTracks = inputStreamRef.current.getAudioTracks(); 126 | audioTracks.forEach(function (track) { 127 | audioStream.addTrack(track); 128 | }); 129 | 130 | const outputStream = new MediaStream(); 131 | [audioStream, videoOutputStream].forEach(function (s) { 132 | s.getTracks().forEach(function (t) { 133 | outputStream.addTrack(t); 134 | }); 135 | }); 136 | 137 | mediaRecorderRef.current = new MediaRecorder(outputStream, { 138 | mimeType: getRecorderMimeType(), 139 | videoBitsPerSecond: 3000000, 140 | audioBitsPerSecond: 64000, 141 | }); 142 | 143 | mediaRecorderRef.current.addEventListener('dataavailable', (e) => { 144 | wsRef.current.send(e.data); 145 | }); 146 | 147 | mediaRecorderRef.current.addEventListener('stop', () => { 148 | stopStreaming(); 149 | wsRef.current.close(); 150 | }); 151 | 152 | mediaRecorderRef.current.start(1000); 153 | }; 154 | 155 | useEffect(() => { 156 | nameRef.current = textOverlay; 157 | }, [textOverlay]); 158 | 159 | useEffect(() => { 160 | return () => { 161 | cancelAnimationFrame(requestAnimationRef.current); 162 | }; 163 | }, []); 164 | 165 | return ( 166 |
167 | 168 | Wocket 169 | 170 | 171 |
172 |

Wocket

173 |

174 | A demo using modern web technologies to broadcast video from a browser 175 | to a server via WebSockets. To learn more, see the Github repo or check out the Mux blog post on the topic. 176 |

177 | 178 |

179 | This service is provided "as is," with no uptime guarantees, support, or any of the usual stuff people pay for. 180 |

181 | 182 | {cameraEnabled && 183 | (streaming ? ( 184 |
185 | 190 | {connected ? 'Connected' : 'Disconnected'} 191 | 192 | setTextOverlay(e.target.value)} 197 | /> 198 | 199 |
200 | ) : ( 201 | <> 202 | setStreamUrl(e.target.value)} 206 | /> 207 | setStreamKey(e.target.value)} 211 | /> 212 | 219 | 220 | ))} 221 |
222 |
227 | {!cameraEnabled && ( 228 | 231 | )} 232 |
233 | 234 |
235 |
236 | 237 |
238 |
239 |
240 | ); 241 | }; 242 | -------------------------------------------------------------------------------- /screenshots/mux-live-stream-dashboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuxLabs/wocket/e6cd9d514b4afde21149093f60d71f84f367befd/screenshots/mux-live-stream-dashboard.gif -------------------------------------------------------------------------------- /screenshots/wocket-live-browser-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuxLabs/wocket/e6cd9d514b4afde21149093f60d71f84f367befd/screenshots/wocket-live-browser-1.png -------------------------------------------------------------------------------- /screenshots/wocket-live-browser-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuxLabs/wocket/e6cd9d514b4afde21149093f60d71f84f367befd/screenshots/wocket-live-browser-2.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const { parse } = require('url'); 4 | const next = require('next'); 5 | const WebSocketServer = require('ws').Server; 6 | const child_process = require('child_process'); 7 | const url = require('url'); 8 | const fs = require('fs'); 9 | 10 | const port = parseInt(process.env.PORT, 10) || 3000; 11 | const host = process.env.HOST || '0.0.0.0'; 12 | const dev = process.env.NODE_ENV !== 'production'; 13 | const app = next({ dev }); 14 | const handle = app.getRequestHandler(); 15 | const cert = process.env.CERT_FILE ? fs.readFileSync(process.env.CERT_FILE) : undefined; 16 | const key = process.env.KEY_FILE ? fs.readFileSync(process.env.KEY_FILE) : undefined; 17 | const transcode = process.env.SMART_TRANSCODE || true; 18 | const options = { 19 | cert, 20 | key 21 | }; 22 | 23 | app.prepare().then(() => { 24 | const server = (cert ? https : http).createServer(options,(req, res) => { 25 | const parsedUrl = parse(req.url, true); 26 | const { pathname, query } = parsedUrl; 27 | 28 | handle(req, res, parsedUrl); 29 | }).listen(port, host, err => { 30 | if (err) throw err; 31 | console.log(`> Ready on port ${port}`); 32 | }); 33 | 34 | const wss = new WebSocketServer({ 35 | server: server 36 | }); 37 | 38 | wss.on('connection', (ws, req) => { 39 | console.log('Streaming socket connected'); 40 | ws.send('WELL HELLO THERE FRIEND'); 41 | 42 | const queryString = url.parse(req.url).search; 43 | const params = new URLSearchParams(queryString); 44 | const baseUrl = params.get('url') ?? 'rtmps://global-live.mux.com/app'; 45 | const key = params.get('key'); 46 | const video = params.get('video'); 47 | const audio = params.get('audio'); 48 | 49 | const rtmpUrl = `${baseUrl}/${key}`; 50 | 51 | const videoCodec = video === 'h264' && !transcode ? 52 | [ '-c:v', 'copy'] : 53 | // video codec config: low latency, adaptive bitrate 54 | ['-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', '-vf', 'scale=w=-2:0']; 55 | 56 | const audioCodec = audio === 'aac' && !transcode ? 57 | [ '-c:a', 'copy'] : 58 | // audio codec config: sampling frequency (11025, 22050, 44100), bitrate 64 kbits 59 | ['-c:a', 'aac', '-ar', '44100', '-b:a', '64k']; 60 | 61 | const ffmpeg = child_process.spawn('ffmpeg', [ 62 | '-i','-', 63 | 64 | //force to overwrite 65 | '-y', 66 | 67 | // used for audio sync 68 | '-use_wallclock_as_timestamps', '1', 69 | '-async', '1', 70 | 71 | ...videoCodec, 72 | 73 | ...audioCodec, 74 | //'-filter_complex', 'aresample=44100', // resample audio to 44100Hz, needed if input is not 44100 75 | //'-strict', 'experimental', 76 | '-bufsize', '1000', 77 | '-f', 'flv', 78 | 79 | rtmpUrl 80 | ]); 81 | 82 | // Kill the WebSocket connection if ffmpeg dies. 83 | ffmpeg.on('close', (code, signal) => { 84 | console.log('FFmpeg child process closed, code ' + code + ', signal ' + signal); 85 | ws.terminate(); 86 | }); 87 | 88 | // Handle STDIN pipe errors by logging to the console. 89 | // These errors most commonly occur when FFmpeg closes and there is still 90 | // data to write.f If left unhandled, the server will crash. 91 | ffmpeg.stdin.on('error', (e) => { 92 | console.log('FFmpeg STDIN Error', e); 93 | }); 94 | 95 | // FFmpeg outputs all of its messages to STDERR. Let's log them to the console. 96 | ffmpeg.stderr.on('data', (data) => { 97 | ws.send('ffmpeg got some data'); 98 | console.log('FFmpeg STDERR:', data.toString()); 99 | }); 100 | 101 | ws.on('message', msg => { 102 | if (Buffer.isBuffer(msg)) { 103 | console.log('this is some video data'); 104 | ffmpeg.stdin.write(msg); 105 | } else { 106 | console.log(msg); 107 | } 108 | }); 109 | 110 | ws.on('close', e => { 111 | console.log('shit got closed, yo'); 112 | ffmpeg.kill('SIGINT'); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /styles/demo.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 0 auto; 3 | display: flex; 4 | height: 100%; 5 | 6 | font-family: Akkurat, 'Helvetica Neue', Helvetica, Arial, sans-serif; 7 | } 8 | 9 | .info { 10 | flex: 1 auto; 11 | display: flex; 12 | flex-direction: column; 13 | height: 100%; 14 | width: 300px; 15 | color: #fff; 16 | background-color: #383838; 17 | padding: 1em; 18 | 19 | border-right: 1px solid #292929; 20 | } 21 | 22 | .info p { 23 | margin-top: 1em; 24 | margin-bottom: 1em; 25 | } 26 | 27 | .info a { 28 | color: #fff; 29 | text-decoration: underline; 30 | } 31 | 32 | .info a:visited { 33 | color: #efefef; 34 | text-decoration: underline; 35 | } 36 | 37 | .info input { 38 | margin-top: 1.5em; 39 | margin-bottom: 1.5em; 40 | width: 100%; 41 | } 42 | 43 | .videoContainer { 44 | position: relative; 45 | width: 100%; 46 | flex: 1 auto; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | } 51 | 52 | .videoContainer .startButton { 53 | font-size: 2em; 54 | margin: 0 auto; 55 | max-width: 50%; 56 | z-index: 5; 57 | } 58 | 59 | .inputVideo { 60 | z-index: 0; 61 | position: absolute; 62 | } 63 | 64 | .inputVideo video { 65 | width: 100%; 66 | height: auto; 67 | visibility: hidden; 68 | } 69 | 70 | .outputCanvas { 71 | z-index: 1; 72 | } 73 | 74 | .outputCanvas canvas { 75 | height: 100%; 76 | width: 100%; 77 | display: none; 78 | } 79 | 80 | .cameraEnabled canvas { 81 | display: block; 82 | } 83 | 84 | .streamStatus { 85 | padding-left: 1.5em; 86 | position: relative; 87 | } 88 | 89 | .streamStatus::before { 90 | position: absolute; 91 | left: 0em; 92 | content: ''; 93 | display: block; 94 | height: 1em; 95 | width: 1em; 96 | border-radius: 1em; 97 | background-color: #efefef; 98 | } 99 | 100 | .streamStatus.connected::before { 101 | background-color: #8fe1d3; 102 | } 103 | 104 | .streamStatus.disconnected::before { 105 | background-color: #fb3c4e; 106 | } 107 | -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body, 7 | body > div { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | font-size: 15px; 13 | font-family: Akkurat, 'Helvetica Neue', Helvetica, Arial, sans-serif; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | h1 { 19 | font-size: 38px; 20 | } 21 | 22 | h2 { 23 | font-size: 28px; 24 | } 25 | 26 | h3 { 27 | font-size: 21px; 28 | } 29 | 30 | h4, 31 | h5, 32 | h6 { 33 | font-size: 18px; 34 | } 35 | 36 | button { 37 | display: block; 38 | width: 100%; 39 | text-align: center; 40 | cursor: pointer; 41 | line-height: 22px; 42 | color: rgb(255, 255, 255); 43 | background-color: transparent; 44 | background-image: linear-gradient( 45 | to right, 46 | rgb(255, 61, 48), 47 | rgb(255, 43, 97) 48 | ); 49 | padding: 14px 10px; 50 | text-decoration: none; 51 | border-radius: 5px; 52 | border-style: none; 53 | } 54 | 55 | input[type=text] { 56 | padding: 14px 20px; 57 | border-radius: 5px; 58 | border: 1px solid #e4e4e4; 59 | box-shadow: inset 0 1px 1px 0 rgba(0,0,0,.06); 60 | } 61 | -------------------------------------------------------------------------------- /styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@next/env@12.2.2": 6 | version "12.2.2" 7 | resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.2.tgz#cc1a0a445bd254499e30f632968c03192455f4cc" 8 | integrity sha512-BqDwE4gDl1F608TpnNxZqrCn6g48MBjvmWFEmeX5wEXDXh3IkAOw6ASKUgjT8H4OUePYFqghDFUss5ZhnbOUjw== 9 | 10 | "@next/swc-android-arm-eabi@12.2.2": 11 | version "12.2.2" 12 | resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.2.tgz#f6c4111e6371f73af6bf80c9accb3d96850a92cd" 13 | integrity sha512-VHjuCHeq9qCprUZbsRxxM/VqSW8MmsUtqB5nEpGEgUNnQi/BTm/2aK8tl7R4D0twGKRh6g1AAeFuWtXzk9Z/vQ== 14 | 15 | "@next/swc-android-arm64@12.2.2": 16 | version "12.2.2" 17 | resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.2.tgz#b69de59c51e631a7600439e7a8993d6e82f3369e" 18 | integrity sha512-v5EYzXUOSv0r9mO/2PX6mOcF53k8ndlu9yeFHVAWW1Dhw2jaJcvTRcCAwYYN8Q3tDg0nH3NbEltJDLKmcJOuVA== 19 | 20 | "@next/swc-darwin-arm64@12.2.2": 21 | version "12.2.2" 22 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.2.tgz#80157c91668eff95b72d052428c353eab0fc4c50" 23 | integrity sha512-JCoGySHKGt+YBk7xRTFGx1QjrnCcwYxIo3yGepcOq64MoiocTM3yllQWeOAJU2/k9MH0+B5E9WUSme4rOCBbpA== 24 | 25 | "@next/swc-darwin-x64@12.2.2": 26 | version "12.2.2" 27 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.2.tgz#12be2f58e676fccff3d48a62921b9927ed295133" 28 | integrity sha512-dztDtvfkhUqiqpXvrWVccfGhLe44yQ5tQ7B4tBfnsOR6vxzI9DNPHTlEOgRN9qDqTAcFyPxvg86mn4l8bB9Jcw== 29 | 30 | "@next/swc-freebsd-x64@12.2.2": 31 | version "12.2.2" 32 | resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.2.tgz#de1363431a49059f1efb8c0f86ce6a79c53b3a95" 33 | integrity sha512-JUnXB+2xfxqsAvhFLPJpU1NeyDsvJrKoOjpV7g3Dxbno2Riu4tDKn3kKF886yleAuD/1qNTUCpqubTvbbT2VoA== 34 | 35 | "@next/swc-linux-arm-gnueabihf@12.2.2": 36 | version "12.2.2" 37 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.2.tgz#d5b8e0d1bb55bbd9db4d2fec018217471dc8b9e6" 38 | integrity sha512-XeYC/qqPLz58R4pjkb+x8sUUxuGLnx9QruC7/IGkK68yW4G17PHwKI/1njFYVfXTXUukpWjcfBuauWwxp9ke7Q== 39 | 40 | "@next/swc-linux-arm64-gnu@12.2.2": 41 | version "12.2.2" 42 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.2.tgz#3bc75984e1d5ec8f59eb53702cc382d8e1be2061" 43 | integrity sha512-d6jT8xgfKYFkzR7J0OHo2D+kFvY/6W8qEo6/hmdrTt6AKAqxs//rbbcdoyn3YQq1x6FVUUd39zzpezZntg9Naw== 44 | 45 | "@next/swc-linux-arm64-musl@12.2.2": 46 | version "12.2.2" 47 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.2.tgz#270db73e07a18d999f61e79a917943fa5bc1ef56" 48 | integrity sha512-rIZRFxI9N/502auJT1i7coas0HTHUM+HaXMyJiCpnY8Rimbo0495ir24tzzHo3nQqJwcflcPTwEh/DV17sdv9A== 49 | 50 | "@next/swc-linux-x64-gnu@12.2.2": 51 | version "12.2.2" 52 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.2.tgz#e6c72fa20478552e898c434f4d4c0c5e89d2ea78" 53 | integrity sha512-ir1vNadlUDj7eQk15AvfhG5BjVizuCHks9uZwBfUgT5jyeDCeRvaDCo1+Q6+0CLOAnYDR/nqSCvBgzG2UdFh9A== 54 | 55 | "@next/swc-linux-x64-musl@12.2.2": 56 | version "12.2.2" 57 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.2.tgz#b9ef9efe2c401839cdefa5e70402386aafdce15a" 58 | integrity sha512-bte5n2GzLN3O8JdSFYWZzMgEgDHZmRz5wiispiiDssj4ik3l8E7wq/czNi8RmIF+ioj2sYVokUNa/ekLzrESWw== 59 | 60 | "@next/swc-win32-arm64-msvc@12.2.2": 61 | version "12.2.2" 62 | resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.2.tgz#18fa7ec7248da3a7926a0601d9ececc53ac83157" 63 | integrity sha512-ZUGCmcDmdPVSAlwJ/aD+1F9lYW8vttseiv4n2+VCDv5JloxiX9aY32kYZaJJO7hmTLNrprvXkb4OvNuHdN22Jg== 64 | 65 | "@next/swc-win32-ia32-msvc@12.2.2": 66 | version "12.2.2" 67 | resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.2.tgz#54936e84f4a219441d051940354da7cd3eafbb4f" 68 | integrity sha512-v7ykeEDbr9eXiblGSZiEYYkWoig6sRhAbLKHUHQtk8vEWWVEqeXFcxmw6LRrKu5rCN1DY357UlYWToCGPQPCRA== 69 | 70 | "@next/swc-win32-x64-msvc@12.2.2": 71 | version "12.2.2" 72 | resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.2.tgz#7460be700a60d75816f01109400b51fe929d7e89" 73 | integrity sha512-2D2iinWUL6xx8D9LYVZ5qi7FP6uLAoWymt8m8aaG2Ld/Ka8/k723fJfiklfuAcwOxfufPJI+nRbT5VcgHGzHAQ== 74 | 75 | "@swc/helpers@0.4.2": 76 | version "0.4.2" 77 | resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.2.tgz#ed1f6997ffbc22396665d9ba74e2a5c0a2d782f8" 78 | integrity sha512-556Az0VX7WR6UdoTn4htt/l3zPQ7bsQWK+HqdG4swV7beUCxo/BqmvbOpUkTIm/9ih86LIf1qsUnywNL3obGHw== 79 | dependencies: 80 | tslib "^2.4.0" 81 | 82 | abbrev@1: 83 | version "1.1.1" 84 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 85 | integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== 86 | 87 | anymatch@~3.1.2: 88 | version "3.1.2" 89 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 90 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 91 | dependencies: 92 | normalize-path "^3.0.0" 93 | picomatch "^2.0.4" 94 | 95 | balanced-match@^1.0.0: 96 | version "1.0.2" 97 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 98 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 99 | 100 | binary-extensions@^2.0.0: 101 | version "2.2.0" 102 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 103 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 104 | 105 | brace-expansion@^1.1.7: 106 | version "1.1.11" 107 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 108 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 109 | dependencies: 110 | balanced-match "^1.0.0" 111 | concat-map "0.0.1" 112 | 113 | braces@~3.0.2: 114 | version "3.0.2" 115 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 116 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 117 | dependencies: 118 | fill-range "^7.0.1" 119 | 120 | caniuse-lite@^1.0.30001332: 121 | version "1.0.30001364" 122 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001364.tgz#1e118f0e933ed2b79f8d461796b8ce45398014a0" 123 | integrity sha512-9O0xzV3wVyX0SlegIQ6knz+okhBB5pE0PC40MNdwcipjwpxoUEHL24uJ+gG42cgklPjfO5ZjZPme9FTSN3QT2Q== 124 | 125 | chokidar@^3.5.2: 126 | version "3.5.3" 127 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 128 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 129 | dependencies: 130 | anymatch "~3.1.2" 131 | braces "~3.0.2" 132 | glob-parent "~5.1.2" 133 | is-binary-path "~2.1.0" 134 | is-glob "~4.0.1" 135 | normalize-path "~3.0.0" 136 | readdirp "~3.6.0" 137 | optionalDependencies: 138 | fsevents "~2.3.2" 139 | 140 | concat-map@0.0.1: 141 | version "0.0.1" 142 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 143 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 144 | 145 | debug@^3.2.7: 146 | version "3.2.7" 147 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" 148 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== 149 | dependencies: 150 | ms "^2.1.1" 151 | 152 | fill-range@^7.0.1: 153 | version "7.0.1" 154 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 155 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 156 | dependencies: 157 | to-regex-range "^5.0.1" 158 | 159 | fsevents@~2.3.2: 160 | version "2.3.2" 161 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 162 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 163 | 164 | glob-parent@~5.1.2: 165 | version "5.1.2" 166 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 167 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 168 | dependencies: 169 | is-glob "^4.0.1" 170 | 171 | has-flag@^3.0.0: 172 | version "3.0.0" 173 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 174 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 175 | 176 | ignore-by-default@^1.0.1: 177 | version "1.0.1" 178 | resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" 179 | integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== 180 | 181 | is-binary-path@~2.1.0: 182 | version "2.1.0" 183 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 184 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 185 | dependencies: 186 | binary-extensions "^2.0.0" 187 | 188 | is-extglob@^2.1.1: 189 | version "2.1.1" 190 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 191 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 192 | 193 | is-glob@^4.0.1, is-glob@~4.0.1: 194 | version "4.0.3" 195 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 196 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 197 | dependencies: 198 | is-extglob "^2.1.1" 199 | 200 | is-number@^7.0.0: 201 | version "7.0.0" 202 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 203 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 204 | 205 | "js-tokens@^3.0.0 || ^4.0.0": 206 | version "4.0.0" 207 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 208 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 209 | 210 | loose-envify@^1.1.0: 211 | version "1.4.0" 212 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 213 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 214 | dependencies: 215 | js-tokens "^3.0.0 || ^4.0.0" 216 | 217 | milligram@^1.3.0: 218 | version "1.4.1" 219 | resolved "https://registry.yarnpkg.com/milligram/-/milligram-1.4.1.tgz#6c8c781541b0d994ccca784c60f0aca1f7104b42" 220 | integrity sha512-RCgh/boHhcXWOUfKJWm3RJRoUeaEguoipDg0mJ31G0tFfvcpWMUlO1Zlqqr12K4kAXfDlllaidu0x7PaL2PTFg== 221 | dependencies: 222 | normalize.css "~8.0.1" 223 | 224 | minimatch@^3.0.4: 225 | version "3.1.2" 226 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 227 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 228 | dependencies: 229 | brace-expansion "^1.1.7" 230 | 231 | ms@^2.1.1: 232 | version "2.1.3" 233 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 234 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 235 | 236 | nanoid@^3.1.30: 237 | version "3.3.4" 238 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 239 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 240 | 241 | next@latest: 242 | version "12.2.2" 243 | resolved "https://registry.yarnpkg.com/next/-/next-12.2.2.tgz#029bf5e4a18a891ca5d05b189b7cd983fd22c072" 244 | integrity sha512-zAYFY45aBry/PlKONqtlloRFqU/We3zWYdn2NoGvDZkoYUYQSJC8WMcalS5C19MxbCZLUVCX7D7a6gTGgl2yLg== 245 | dependencies: 246 | "@next/env" "12.2.2" 247 | "@swc/helpers" "0.4.2" 248 | caniuse-lite "^1.0.30001332" 249 | postcss "8.4.5" 250 | styled-jsx "5.0.2" 251 | use-sync-external-store "1.1.0" 252 | optionalDependencies: 253 | "@next/swc-android-arm-eabi" "12.2.2" 254 | "@next/swc-android-arm64" "12.2.2" 255 | "@next/swc-darwin-arm64" "12.2.2" 256 | "@next/swc-darwin-x64" "12.2.2" 257 | "@next/swc-freebsd-x64" "12.2.2" 258 | "@next/swc-linux-arm-gnueabihf" "12.2.2" 259 | "@next/swc-linux-arm64-gnu" "12.2.2" 260 | "@next/swc-linux-arm64-musl" "12.2.2" 261 | "@next/swc-linux-x64-gnu" "12.2.2" 262 | "@next/swc-linux-x64-musl" "12.2.2" 263 | "@next/swc-win32-arm64-msvc" "12.2.2" 264 | "@next/swc-win32-ia32-msvc" "12.2.2" 265 | "@next/swc-win32-x64-msvc" "12.2.2" 266 | 267 | nodemon@^2.0.2: 268 | version "2.0.19" 269 | resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.19.tgz#cac175f74b9cb8b57e770d47841995eebe4488bd" 270 | integrity sha512-4pv1f2bMDj0Eeg/MhGqxrtveeQ5/G/UVe9iO6uTZzjnRluSA4PVWf8CW99LUPwGB3eNIA7zUFoP77YuI7hOc0A== 271 | dependencies: 272 | chokidar "^3.5.2" 273 | debug "^3.2.7" 274 | ignore-by-default "^1.0.1" 275 | minimatch "^3.0.4" 276 | pstree.remy "^1.1.8" 277 | semver "^5.7.1" 278 | simple-update-notifier "^1.0.7" 279 | supports-color "^5.5.0" 280 | touch "^3.1.0" 281 | undefsafe "^2.0.5" 282 | 283 | nopt@~1.0.10: 284 | version "1.0.10" 285 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" 286 | integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== 287 | dependencies: 288 | abbrev "1" 289 | 290 | normalize-path@^3.0.0, normalize-path@~3.0.0: 291 | version "3.0.0" 292 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 293 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 294 | 295 | normalize.css@~8.0.1: 296 | version "8.0.1" 297 | resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" 298 | integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== 299 | 300 | picocolors@^1.0.0: 301 | version "1.0.0" 302 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 303 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 304 | 305 | picomatch@^2.0.4, picomatch@^2.2.1: 306 | version "2.3.1" 307 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 308 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 309 | 310 | postcss@8.4.5: 311 | version "8.4.5" 312 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" 313 | integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== 314 | dependencies: 315 | nanoid "^3.1.30" 316 | picocolors "^1.0.0" 317 | source-map-js "^1.0.1" 318 | 319 | pstree.remy@^1.1.8: 320 | version "1.1.8" 321 | resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" 322 | integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== 323 | 324 | react-dom@latest: 325 | version "18.2.0" 326 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" 327 | integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== 328 | dependencies: 329 | loose-envify "^1.1.0" 330 | scheduler "^0.23.0" 331 | 332 | react@latest: 333 | version "18.2.0" 334 | resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" 335 | integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== 336 | dependencies: 337 | loose-envify "^1.1.0" 338 | 339 | readdirp@~3.6.0: 340 | version "3.6.0" 341 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 342 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 343 | dependencies: 344 | picomatch "^2.2.1" 345 | 346 | scheduler@^0.23.0: 347 | version "0.23.0" 348 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" 349 | integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== 350 | dependencies: 351 | loose-envify "^1.1.0" 352 | 353 | semver@^5.7.1: 354 | version "5.7.1" 355 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 356 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 357 | 358 | semver@~7.0.0: 359 | version "7.0.0" 360 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" 361 | integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== 362 | 363 | simple-update-notifier@^1.0.7: 364 | version "1.0.7" 365 | resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc" 366 | integrity sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew== 367 | dependencies: 368 | semver "~7.0.0" 369 | 370 | source-map-js@^1.0.1: 371 | version "1.0.2" 372 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 373 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 374 | 375 | styled-jsx@5.0.2: 376 | version "5.0.2" 377 | resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729" 378 | integrity sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ== 379 | 380 | supports-color@^5.5.0: 381 | version "5.5.0" 382 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 383 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 384 | dependencies: 385 | has-flag "^3.0.0" 386 | 387 | to-regex-range@^5.0.1: 388 | version "5.0.1" 389 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 390 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 391 | dependencies: 392 | is-number "^7.0.0" 393 | 394 | touch@^3.1.0: 395 | version "3.1.0" 396 | resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" 397 | integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== 398 | dependencies: 399 | nopt "~1.0.10" 400 | 401 | tslib@^2.4.0: 402 | version "2.4.0" 403 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" 404 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== 405 | 406 | undefsafe@^2.0.5: 407 | version "2.0.5" 408 | resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" 409 | integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== 410 | 411 | use-sync-external-store@1.1.0: 412 | version "1.1.0" 413 | resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82" 414 | integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ== 415 | 416 | ws@^7.2.3: 417 | version "7.5.8" 418 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a" 419 | integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== 420 | --------------------------------------------------------------------------------