├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── examples ├── webrtmp-react │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ └── yarn.lock └── webrtmp-static │ ├── README.md │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package.json │ └── yarn.lock ├── package.json ├── src ├── CastSession.ts ├── index.ts ├── web.ts ├── webrtc.ts └── websocket.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | 3 | lib/ 4 | 5 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | tsconfig.json 3 | src 4 | .prettierrc 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "semi": false, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Livepeer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [[ DEPRECATED ]] webrtmp-sdk [![npm version](https://badge.fury.io/js/@livepeer%2Fwebrtmp-sdk.svg)](https://badge.fury.io/js/@livepeer%2Fwebrtmp-sdk) 2 | 3 | > THIS SDK IS DEPRECATED. REACT DEVELOPERS SHOULD USE THE [NEW BROADCAST SDK](https://docs.livepeer.org/reference/livepeer-js/Broadcast), AND ALL OTHER DEVELOPERS SHOULD FOLLOW THIS [GUIDE ON WEBRTC BROADCASTING](https://docs.livepeer.org/guides/developing/stream-via-browser.en-US#adding-broadcasting-with-plain-webrtc) 4 | 5 | JavaScript SDK for streaming media via RTMP from the Web. Originally designed 6 | for [Livepeer.com](livepeer.com), but can be used for any other service by 7 | running your own [webrtmp-server](https://github.com/livepeer/webrtmp-server). 8 | 9 | > This SDK works best on Chrome Desktop, as it currently only supports WebSocket on H.264-capable browsers. We are working on WebRTC support to allow the use of non-Chrome and non-Desktop browsers. Check out the [Browser Support](#browser-support) section for more. 10 | 11 | 12 | ## Installation 13 | 14 | ### CDN 15 | 16 | Add the following script tag to the header of your HTML file: 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | The API will be available as a global named `webRTMP`: 23 | 24 | ```js 25 | const { Client } = webRTMP 26 | ``` 27 | 28 | ### Package Managers 29 | 30 | #### yarn 31 | 32 | ```sh 33 | yarn add @livepeer/webrtmp-sdk 34 | ``` 35 | 36 | #### npm 37 | ```sh 38 | npm install @livepeer/webrtmp-sdk 39 | ``` 40 | 41 | The API can then be imported as a regular module: 42 | 43 | ```js 44 | const { Client } = require('webrtmp-sdk') 45 | ``` 46 | 47 | ## Usage 48 | 49 | In order to stream through Livepeer, you are going to need a secret `streamKey`, 50 | which can be obtained by following these steps: 51 | 52 | 1) Create Livepeer Account at [livepeer.com](https://www.livepeer.com); 53 | 2) Go to the Livepeer [Streams Dashboard](https://www.livepeer.com/dashboard/streams); 54 | 3) Create a stream; 55 | 4) Grab the stream key and replace the `{{STREAM_KEY}}` in the example below. 56 | 57 | 58 | ```js 59 | const client = new Client() 60 | 61 | async function start() { 62 | const streamKey = '{{STREAM_KEY}}' 63 | 64 | const stream = await navigator.mediaDevices.getUserMedia({ 65 | video: true, 66 | audio: true 67 | }) 68 | 69 | const session = client.cast(stream, streamKey) 70 | 71 | session.on('open', () => { 72 | console.log('Stream started.') 73 | }) 74 | 75 | session.on('close', () => { 76 | console.log('Stream stopped.') 77 | }) 78 | 79 | session.on('error', (err) => { 80 | console.log('Stream error.', err.message) 81 | }) 82 | } 83 | 84 | start() 85 | ``` 86 | 87 | > **NOTE:** If you have multiple streaming users you will need a separate 88 | > `streamKey` for each of them. So you should have a backend service 89 | > programmatically create a stream through Livepeer API and return the 90 | > `streamKey` for your front-end. Check out [Livepeer API 91 | > Documentation](https://livepeer.com/docs/guides) on how to [get an API 92 | > key](https://livepeer.com/docs/guides/start-live-streaming/api-key) and then 93 | > how to [create a stream](https://livepeer.com/docs/guides/start-live-streaming/create-a-stream). 94 | 95 | ## Browser Support 96 | 97 | We provide a utility function to check whether the current browser is supported by the SDK: 98 | 99 | ```js 100 | const { isSupported } = require('@livepeer/webrtmp-sdk') 101 | 102 | if (!isSupported()) { 103 | alert('webrtmp-sdk is not currently supported on this browser') 104 | } 105 | ``` 106 | 107 | ## Examples 108 | 109 | The `examples` folder at the root of this repository contains two projects: 110 | - [webrtmp-static](examples/webrtmp-static), implemented in vanilla HTML, CSS 111 | and JavaScript. Check it out on 112 | [CodePen](https://codepen.io/samuelmtimbo/pen/QWgaZGL). 113 | - [webrtmp-react](examples/webrtmp-react), implemented with React (created 114 | using [create-react-app](https://github.com/facebook/create-react-app)). 115 | 116 | For a full working example, check out [justcast.it](https://justcast.it) ([source 117 | code](https://github.com/victorges/justcast.it)). 118 | 119 | ## Contributing 120 | 121 | Pull Requests are always welcome! 122 | 123 | ## License 124 | 125 | MIT 126 | 127 | -------------------------------------------------------------------------------- /examples/webrtmp-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/webrtmp-react/README.md: -------------------------------------------------------------------------------- 1 | # webrtmp-react 2 | 3 | React app example using webrtmp-sdk to stream user's webcam video through 4 | Livepeer. 5 | -------------------------------------------------------------------------------- /examples/webrtmp-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtmp-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@livepeer/webrtmp-sdk": "^0.2.3", 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-scripts": "4.0.3", 13 | "web-vitals": "^1.0.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/webrtmp-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/webrtmp-sdk/c9dcb4f3d3c0009b2039dace43515bc719c59ff6/examples/webrtmp-react/public/favicon.ico -------------------------------------------------------------------------------- /examples/webrtmp-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/webrtmp-react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/webrtmp-sdk/c9dcb4f3d3c0009b2039dace43515bc719c59ff6/examples/webrtmp-react/public/logo192.png -------------------------------------------------------------------------------- /examples/webrtmp-react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/webrtmp-sdk/c9dcb4f3d3c0009b2039dace43515bc719c59ff6/examples/webrtmp-react/public/logo512.png -------------------------------------------------------------------------------- /examples/webrtmp-react/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 | -------------------------------------------------------------------------------- /examples/webrtmp-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/webrtmp-react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | background-color: #161618; 4 | min-height: 100vh; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | font-size: calc(10px + 2vmin); 10 | color: rgb(237, 237, 239); 11 | } 12 | 13 | .App-input { 14 | height: 42px; 15 | width: 300px; 16 | background: none; 17 | border: 1px solid rgb(158, 140, 252); 18 | margin: 6px; 19 | color: rgb(237, 237, 239); 20 | font-size: 21px; 21 | text-align: center; 22 | border-radius: 3px; 23 | } 24 | 25 | .App-video { 26 | height: 300px; 27 | width: 400px; 28 | border: 1px solid rgb(158, 140, 252); 29 | margin: 30px; 30 | background-color: black; 31 | border-radius: 3px; 32 | } 33 | 34 | .App-button { 35 | height: 42px; 36 | width: 60px; 37 | font-size: large; 38 | border-radius: 3px; 39 | border: 1px solid rgb(158, 140, 252); 40 | color: rgb(237, 237, 239); 41 | background: none; 42 | cursor: pointer; 43 | } 44 | -------------------------------------------------------------------------------- /examples/webrtmp-react/src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import './App.css' 3 | import { Client } from '@livepeer/webrtmp-sdk' 4 | 5 | function App() { 6 | const inputEl = useRef(null) 7 | const videoEl = useRef(null) 8 | const stream = useRef(null) 9 | 10 | useEffect(() => { 11 | ;(async () => { 12 | videoEl.current.volume = 0 13 | 14 | stream.current = await navigator.mediaDevices.getUserMedia({ 15 | video: true, 16 | audio: true, 17 | }) 18 | 19 | videoEl.current.srcObject = stream.current 20 | videoEl.current.play() 21 | })() 22 | }) 23 | 24 | const onButtonClick = async () => { 25 | const streamKey = inputEl.current.value 26 | 27 | if (!stream.current) { 28 | alert('Video stream was not started.') 29 | } 30 | 31 | if (!streamKey) { 32 | alert('Invalid streamKey.') 33 | return 34 | } 35 | 36 | const client = new Client() 37 | 38 | const session = client.cast(stream.current, streamKey) 39 | 40 | session.on('open', () => { 41 | console.log('Stream started.') 42 | alert('Stream started; visit Livepeer Dashboard.') 43 | }) 44 | 45 | session.on('close', () => { 46 | console.log('Stream stopped.') 47 | }) 48 | 49 | session.on('error', (err) => { 50 | console.log('Stream error.', err.message) 51 | }) 52 | } 53 | 54 | return ( 55 |
56 | 62 |
67 | ) 68 | } 69 | 70 | export default App 71 | -------------------------------------------------------------------------------- /examples/webrtmp-react/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 | -------------------------------------------------------------------------------- /examples/webrtmp-react/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 | overscroll-behavior: none; 9 | } 10 | -------------------------------------------------------------------------------- /examples/webrtmp-react/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 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /examples/webrtmp-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/webrtmp-react/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 | -------------------------------------------------------------------------------- /examples/webrtmp-react/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 | -------------------------------------------------------------------------------- /examples/webrtmp-static/README.md: -------------------------------------------------------------------------------- 1 | # webrtmp-static 2 | 3 | Static web page (Vanilla HTML, CSS and JavaScript) example using `webrtmp-sdk` 4 | to stream user's webcam video through Livepeer. 5 | 6 | Check it out on [CodePen](https://codepen.io/samuelmtimbo/pen/QWgaZGL). 7 | -------------------------------------------------------------------------------- /examples/webrtmp-static/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 | overscroll-behavior: none; 9 | } 10 | 11 | #root { 12 | text-align: center; 13 | background-color: #161618; 14 | min-height: 100vh; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | font-size: calc(10px + 2vmin); 20 | color: rgb(237, 237, 239); 21 | } 22 | 23 | #input { 24 | height: 42px; 25 | width: 300px; 26 | background: none; 27 | border: 1px solid rgb(158, 140, 252); 28 | margin: 6px; 29 | color: rgb(237, 237, 239); 30 | font-size: 21px; 31 | text-align: center; 32 | border-radius: 3px; 33 | } 34 | 35 | #video { 36 | height: 300px; 37 | width: 400px; 38 | border: 1px solid rgb(158, 140, 252); 39 | margin: 30px; 40 | background-color: black; 41 | border-radius: 3px; 42 | } 43 | 44 | #button { 45 | height: 42px; 46 | width: 60px; 47 | font-size: large; 48 | border-radius: 3px; 49 | border: 1px solid rgb(158, 140, 252); 50 | color: rgb(237, 237, 239); 51 | background: none; 52 | cursor: pointer; 53 | } 54 | -------------------------------------------------------------------------------- /examples/webrtmp-static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | webrtmp-static 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/webrtmp-static/index.js: -------------------------------------------------------------------------------- 1 | const input = document.getElementById('input') 2 | const video = document.getElementById('video') 3 | const button = document.getElementById('button') 4 | 5 | video.volume = 0 6 | 7 | let stream 8 | 9 | const { Client } = webRTMP 10 | 11 | async function setup() { 12 | stream = await navigator.mediaDevices.getUserMedia({ 13 | video: true, 14 | audio: true, 15 | }) 16 | 17 | video.srcObject = stream 18 | video.play() 19 | } 20 | 21 | setup() 22 | 23 | button.onclick = () => { 24 | if (!stream) { 25 | alert('Video stream was not started.') 26 | } 27 | 28 | const streamKey = input.value 29 | 30 | if (!streamKey) { 31 | alert('Invalid streamKey.') 32 | return 33 | } 34 | 35 | const client = new Client() 36 | 37 | const session = client.cast(stream, streamKey) 38 | 39 | session.on('open', () => { 40 | console.log('Stream started.') 41 | alert('Stream started; visit Livepeer Dashboard.') 42 | }) 43 | 44 | session.on('close', () => { 45 | console.log('Stream stopped.') 46 | }) 47 | 48 | session.on('error', (err) => { 49 | console.log('Stream error.', err.message) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /examples/webrtmp-static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtmp-static", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "serve ." 8 | }, 9 | "devDependencies": { 10 | "serve": "12.0.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/webrtmp-static/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@zeit/schemas@2.6.0": 6 | version "2.6.0" 7 | resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.6.0.tgz#004e8e553b4cd53d538bd38eac7bcbf58a867fe3" 8 | integrity sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg== 9 | 10 | accepts@~1.3.5: 11 | version "1.3.7" 12 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 13 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== 14 | dependencies: 15 | mime-types "~2.1.24" 16 | negotiator "0.6.2" 17 | 18 | ajv@6.12.6: 19 | version "6.12.6" 20 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 21 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 22 | dependencies: 23 | fast-deep-equal "^3.1.1" 24 | fast-json-stable-stringify "^2.0.0" 25 | json-schema-traverse "^0.4.1" 26 | uri-js "^4.2.2" 27 | 28 | ansi-align@^2.0.0: 29 | version "2.0.0" 30 | resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" 31 | integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= 32 | dependencies: 33 | string-width "^2.0.0" 34 | 35 | ansi-regex@^3.0.0: 36 | version "3.0.0" 37 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 38 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 39 | 40 | ansi-styles@^3.2.1: 41 | version "3.2.1" 42 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 43 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 44 | dependencies: 45 | color-convert "^1.9.0" 46 | 47 | arch@^2.1.1: 48 | version "2.2.0" 49 | resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" 50 | integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== 51 | 52 | arg@2.0.0: 53 | version "2.0.0" 54 | resolved "https://registry.yarnpkg.com/arg/-/arg-2.0.0.tgz#c06e7ff69ab05b3a4a03ebe0407fac4cba657545" 55 | integrity sha512-XxNTUzKnz1ctK3ZIcI2XUPlD96wbHP2nGqkPKpvk/HNRlPveYrXIVSTk9m3LcqOgDPg3B1nMvdV/K8wZd7PG4w== 56 | 57 | balanced-match@^1.0.0: 58 | version "1.0.2" 59 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 60 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 61 | 62 | boxen@1.3.0: 63 | version "1.3.0" 64 | resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" 65 | integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== 66 | dependencies: 67 | ansi-align "^2.0.0" 68 | camelcase "^4.0.0" 69 | chalk "^2.0.1" 70 | cli-boxes "^1.0.0" 71 | string-width "^2.0.0" 72 | term-size "^1.2.0" 73 | widest-line "^2.0.0" 74 | 75 | brace-expansion@^1.1.7: 76 | version "1.1.11" 77 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 78 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 79 | dependencies: 80 | balanced-match "^1.0.0" 81 | concat-map "0.0.1" 82 | 83 | bytes@3.0.0: 84 | version "3.0.0" 85 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 86 | integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= 87 | 88 | camelcase@^4.0.0: 89 | version "4.1.0" 90 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" 91 | integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= 92 | 93 | chalk@2.4.1: 94 | version "2.4.1" 95 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" 96 | integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== 97 | dependencies: 98 | ansi-styles "^3.2.1" 99 | escape-string-regexp "^1.0.5" 100 | supports-color "^5.3.0" 101 | 102 | chalk@^2.0.1: 103 | version "2.4.2" 104 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 105 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 106 | dependencies: 107 | ansi-styles "^3.2.1" 108 | escape-string-regexp "^1.0.5" 109 | supports-color "^5.3.0" 110 | 111 | cli-boxes@^1.0.0: 112 | version "1.0.0" 113 | resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" 114 | integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= 115 | 116 | clipboardy@2.3.0: 117 | version "2.3.0" 118 | resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290" 119 | integrity sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ== 120 | dependencies: 121 | arch "^2.1.1" 122 | execa "^1.0.0" 123 | is-wsl "^2.1.1" 124 | 125 | color-convert@^1.9.0: 126 | version "1.9.3" 127 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 128 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 129 | dependencies: 130 | color-name "1.1.3" 131 | 132 | color-name@1.1.3: 133 | version "1.1.3" 134 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 135 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 136 | 137 | compressible@~2.0.14: 138 | version "2.0.18" 139 | resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" 140 | integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== 141 | dependencies: 142 | mime-db ">= 1.43.0 < 2" 143 | 144 | compression@1.7.3: 145 | version "1.7.3" 146 | resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" 147 | integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg== 148 | dependencies: 149 | accepts "~1.3.5" 150 | bytes "3.0.0" 151 | compressible "~2.0.14" 152 | debug "2.6.9" 153 | on-headers "~1.0.1" 154 | safe-buffer "5.1.2" 155 | vary "~1.1.2" 156 | 157 | concat-map@0.0.1: 158 | version "0.0.1" 159 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 160 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 161 | 162 | content-disposition@0.5.2: 163 | version "0.5.2" 164 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 165 | integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= 166 | 167 | cross-spawn@^5.0.1: 168 | version "5.1.0" 169 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" 170 | integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= 171 | dependencies: 172 | lru-cache "^4.0.1" 173 | shebang-command "^1.2.0" 174 | which "^1.2.9" 175 | 176 | cross-spawn@^6.0.0: 177 | version "6.0.5" 178 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" 179 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== 180 | dependencies: 181 | nice-try "^1.0.4" 182 | path-key "^2.0.1" 183 | semver "^5.5.0" 184 | shebang-command "^1.2.0" 185 | which "^1.2.9" 186 | 187 | debug@2.6.9: 188 | version "2.6.9" 189 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 190 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 191 | dependencies: 192 | ms "2.0.0" 193 | 194 | deep-extend@^0.6.0: 195 | version "0.6.0" 196 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" 197 | integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== 198 | 199 | end-of-stream@^1.1.0: 200 | version "1.4.4" 201 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 202 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 203 | dependencies: 204 | once "^1.4.0" 205 | 206 | escape-string-regexp@^1.0.5: 207 | version "1.0.5" 208 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 209 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 210 | 211 | execa@^0.7.0: 212 | version "0.7.0" 213 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" 214 | integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= 215 | dependencies: 216 | cross-spawn "^5.0.1" 217 | get-stream "^3.0.0" 218 | is-stream "^1.1.0" 219 | npm-run-path "^2.0.0" 220 | p-finally "^1.0.0" 221 | signal-exit "^3.0.0" 222 | strip-eof "^1.0.0" 223 | 224 | execa@^1.0.0: 225 | version "1.0.0" 226 | resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" 227 | integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== 228 | dependencies: 229 | cross-spawn "^6.0.0" 230 | get-stream "^4.0.0" 231 | is-stream "^1.1.0" 232 | npm-run-path "^2.0.0" 233 | p-finally "^1.0.0" 234 | signal-exit "^3.0.0" 235 | strip-eof "^1.0.0" 236 | 237 | fast-deep-equal@^3.1.1: 238 | version "3.1.3" 239 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 240 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 241 | 242 | fast-json-stable-stringify@^2.0.0: 243 | version "2.1.0" 244 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 245 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 246 | 247 | fast-url-parser@1.1.3: 248 | version "1.1.3" 249 | resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" 250 | integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0= 251 | dependencies: 252 | punycode "^1.3.2" 253 | 254 | get-stream@^3.0.0: 255 | version "3.0.0" 256 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" 257 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= 258 | 259 | get-stream@^4.0.0: 260 | version "4.1.0" 261 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" 262 | integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== 263 | dependencies: 264 | pump "^3.0.0" 265 | 266 | has-flag@^3.0.0: 267 | version "3.0.0" 268 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 269 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 270 | 271 | ini@~1.3.0: 272 | version "1.3.8" 273 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" 274 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== 275 | 276 | is-docker@^2.0.0: 277 | version "2.2.1" 278 | resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" 279 | integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== 280 | 281 | is-fullwidth-code-point@^2.0.0: 282 | version "2.0.0" 283 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 284 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 285 | 286 | is-stream@^1.1.0: 287 | version "1.1.0" 288 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 289 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= 290 | 291 | is-wsl@^2.1.1: 292 | version "2.2.0" 293 | resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" 294 | integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== 295 | dependencies: 296 | is-docker "^2.0.0" 297 | 298 | isexe@^2.0.0: 299 | version "2.0.0" 300 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 301 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 302 | 303 | json-schema-traverse@^0.4.1: 304 | version "0.4.1" 305 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 306 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 307 | 308 | lru-cache@^4.0.1: 309 | version "4.1.5" 310 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" 311 | integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== 312 | dependencies: 313 | pseudomap "^1.0.2" 314 | yallist "^2.1.2" 315 | 316 | mime-db@1.49.0: 317 | version "1.49.0" 318 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" 319 | integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== 320 | 321 | "mime-db@>= 1.43.0 < 2": 322 | version "1.50.0" 323 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" 324 | integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== 325 | 326 | mime-db@~1.33.0: 327 | version "1.33.0" 328 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 329 | integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== 330 | 331 | mime-types@2.1.18: 332 | version "2.1.18" 333 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 334 | integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== 335 | dependencies: 336 | mime-db "~1.33.0" 337 | 338 | mime-types@~2.1.24: 339 | version "2.1.32" 340 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" 341 | integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== 342 | dependencies: 343 | mime-db "1.49.0" 344 | 345 | minimatch@3.0.4: 346 | version "3.0.4" 347 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 348 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 349 | dependencies: 350 | brace-expansion "^1.1.7" 351 | 352 | minimist@^1.2.0: 353 | version "1.2.5" 354 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 355 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 356 | 357 | ms@2.0.0: 358 | version "2.0.0" 359 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 360 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 361 | 362 | negotiator@0.6.2: 363 | version "0.6.2" 364 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 365 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== 366 | 367 | nice-try@^1.0.4: 368 | version "1.0.5" 369 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" 370 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 371 | 372 | npm-run-path@^2.0.0: 373 | version "2.0.2" 374 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" 375 | integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= 376 | dependencies: 377 | path-key "^2.0.0" 378 | 379 | on-headers@~1.0.1: 380 | version "1.0.2" 381 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" 382 | integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== 383 | 384 | once@^1.3.1, once@^1.4.0: 385 | version "1.4.0" 386 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 387 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 388 | dependencies: 389 | wrappy "1" 390 | 391 | p-finally@^1.0.0: 392 | version "1.0.0" 393 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 394 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= 395 | 396 | path-is-inside@1.0.2: 397 | version "1.0.2" 398 | resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" 399 | integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= 400 | 401 | path-key@^2.0.0, path-key@^2.0.1: 402 | version "2.0.1" 403 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 404 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= 405 | 406 | path-to-regexp@2.2.1: 407 | version "2.2.1" 408 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" 409 | integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== 410 | 411 | pseudomap@^1.0.2: 412 | version "1.0.2" 413 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 414 | integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= 415 | 416 | pump@^3.0.0: 417 | version "3.0.0" 418 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 419 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 420 | dependencies: 421 | end-of-stream "^1.1.0" 422 | once "^1.3.1" 423 | 424 | punycode@^1.3.2: 425 | version "1.4.1" 426 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 427 | integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= 428 | 429 | punycode@^2.1.0: 430 | version "2.1.1" 431 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 432 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 433 | 434 | range-parser@1.2.0: 435 | version "1.2.0" 436 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 437 | integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= 438 | 439 | rc@^1.0.1, rc@^1.1.6: 440 | version "1.2.8" 441 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" 442 | integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== 443 | dependencies: 444 | deep-extend "^0.6.0" 445 | ini "~1.3.0" 446 | minimist "^1.2.0" 447 | strip-json-comments "~2.0.1" 448 | 449 | registry-auth-token@3.3.2: 450 | version "3.3.2" 451 | resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20" 452 | integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ== 453 | dependencies: 454 | rc "^1.1.6" 455 | safe-buffer "^5.0.1" 456 | 457 | registry-url@3.1.0: 458 | version "3.1.0" 459 | resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" 460 | integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= 461 | dependencies: 462 | rc "^1.0.1" 463 | 464 | safe-buffer@5.1.2: 465 | version "5.1.2" 466 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 467 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 468 | 469 | safe-buffer@^5.0.1: 470 | version "5.2.1" 471 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 472 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 473 | 474 | semver@^5.5.0: 475 | version "5.7.1" 476 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 477 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 478 | 479 | serve-handler@6.1.3: 480 | version "6.1.3" 481 | resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8" 482 | integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w== 483 | dependencies: 484 | bytes "3.0.0" 485 | content-disposition "0.5.2" 486 | fast-url-parser "1.1.3" 487 | mime-types "2.1.18" 488 | minimatch "3.0.4" 489 | path-is-inside "1.0.2" 490 | path-to-regexp "2.2.1" 491 | range-parser "1.2.0" 492 | 493 | serve@12.0.1: 494 | version "12.0.1" 495 | resolved "https://registry.yarnpkg.com/serve/-/serve-12.0.1.tgz#5b0e05849f5ed9b8aab0f30a298c3664bba052bb" 496 | integrity sha512-CQ4ikLpxg/wmNM7yivulpS6fhjRiFG6OjmP8ty3/c1SBnSk23fpKmLAV4HboTA2KrZhkUPlDfjDhnRmAjQ5Phw== 497 | dependencies: 498 | "@zeit/schemas" "2.6.0" 499 | ajv "6.12.6" 500 | arg "2.0.0" 501 | boxen "1.3.0" 502 | chalk "2.4.1" 503 | clipboardy "2.3.0" 504 | compression "1.7.3" 505 | serve-handler "6.1.3" 506 | update-check "1.5.2" 507 | 508 | shebang-command@^1.2.0: 509 | version "1.2.0" 510 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 511 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= 512 | dependencies: 513 | shebang-regex "^1.0.0" 514 | 515 | shebang-regex@^1.0.0: 516 | version "1.0.0" 517 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 518 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 519 | 520 | signal-exit@^3.0.0: 521 | version "3.0.4" 522 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.4.tgz#366a4684d175b9cab2081e3681fda3747b6c51d7" 523 | integrity sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q== 524 | 525 | string-width@^2.0.0, string-width@^2.1.1: 526 | version "2.1.1" 527 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 528 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 529 | dependencies: 530 | is-fullwidth-code-point "^2.0.0" 531 | strip-ansi "^4.0.0" 532 | 533 | strip-ansi@^4.0.0: 534 | version "4.0.0" 535 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 536 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 537 | dependencies: 538 | ansi-regex "^3.0.0" 539 | 540 | strip-eof@^1.0.0: 541 | version "1.0.0" 542 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 543 | integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= 544 | 545 | strip-json-comments@~2.0.1: 546 | version "2.0.1" 547 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 548 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 549 | 550 | supports-color@^5.3.0: 551 | version "5.5.0" 552 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 553 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 554 | dependencies: 555 | has-flag "^3.0.0" 556 | 557 | term-size@^1.2.0: 558 | version "1.2.0" 559 | resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" 560 | integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= 561 | dependencies: 562 | execa "^0.7.0" 563 | 564 | update-check@1.5.2: 565 | version "1.5.2" 566 | resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.2.tgz#2fe09f725c543440b3d7dabe8971f2d5caaedc28" 567 | integrity sha512-1TrmYLuLj/5ZovwUS7fFd1jMH3NnFDN1y1A8dboedIDt7zs/zJMo6TwwlhYKkSeEwzleeiSBV5/3c9ufAQWDaQ== 568 | dependencies: 569 | registry-auth-token "3.3.2" 570 | registry-url "3.1.0" 571 | 572 | uri-js@^4.2.2: 573 | version "4.4.1" 574 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 575 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 576 | dependencies: 577 | punycode "^2.1.0" 578 | 579 | vary@~1.1.2: 580 | version "1.1.2" 581 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 582 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 583 | 584 | which@^1.2.9: 585 | version "1.3.1" 586 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 587 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 588 | dependencies: 589 | isexe "^2.0.0" 590 | 591 | widest-line@^2.0.0: 592 | version "2.0.1" 593 | resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" 594 | integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== 595 | dependencies: 596 | string-width "^2.1.1" 597 | 598 | wrappy@1: 599 | version "1.0.2" 600 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 601 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 602 | 603 | yallist@^2.1.2: 604 | version "2.1.2" 605 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 606 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 607 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livepeer/webrtmp-sdk", 3 | "version": "0.2.5", 4 | "description": "JavaScript SDK for streaming media via RTMP from the Web.", 5 | "main": "lib/index.js", 6 | "repository": "git@github.com:livepeer/webrtmp-sdk.git", 7 | "author": "Samuel Timbó ", 8 | "license": "MIT", 9 | "scripts": { 10 | "lib": "rm -rf lib; tsc", 11 | "build": "esbuild --bundle --minify --log-level=error ./src/web.ts --outfile=./dist/index.js" 12 | }, 13 | "dependencies": { 14 | "events": "3.3.0" 15 | }, 16 | "devDependencies": { 17 | "@types/events": "^3.0.0", 18 | "esbuild": "0.12.28", 19 | "prettier": "2.4.0", 20 | "typescript": "4.4.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/CastSession.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Listener } from 'events' 2 | 3 | export class CastSession extends EventEmitter { 4 | constructor(private _closeFunc: () => void) { 5 | super() 6 | } 7 | 8 | on(type: 'open', listener: () => void): this 9 | on(type: 'close', listener: () => void): this 10 | on(type: 'error', listener: (err: Error) => void): this 11 | on(type: 'open' | 'close' | 'error', listener: Listener): this { 12 | return super.on(type, listener) 13 | } 14 | 15 | close() { 16 | this._closeFunc() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { CastSession } from './CastSession' 2 | import castViaWebRTC from './webrtc' 3 | import castViaWebSocket, { getMimeType } from './websocket' 4 | 5 | export type Protocol = 'ws' | 'wrtc' | 'auto' 6 | 7 | export class Client { 8 | private readonly secure: boolean 9 | private readonly baseUrl: string 10 | private readonly transport: Protocol 11 | 12 | constructor( 13 | opt: { 14 | secure?: boolean 15 | baseUrl?: string 16 | transport?: Protocol 17 | } = {} 18 | ) { 19 | const { 20 | secure = true, 21 | baseUrl = 'origin.livepeer.com/webrtmp', 22 | transport = 'ws', 23 | } = opt 24 | 25 | this.secure = secure 26 | this.baseUrl = baseUrl 27 | this.transport = transport 28 | } 29 | 30 | cast(stream: MediaStream, streamKey: string): CastSession { 31 | if (!streamKey) { 32 | throw new Error('Invalid streamKey.') 33 | } 34 | 35 | let { transport } = this 36 | if (transport === 'auto') { 37 | transport = isSupported() ? 'ws' : 'wrtc' 38 | } 39 | if (transport === 'ws') { 40 | return castViaWebSocket(this.secure, this.baseUrl, stream, streamKey) 41 | } else if (transport === 'wrtc') { 42 | return castViaWebRTC(this.secure, this.baseUrl, stream, streamKey) 43 | } else { 44 | throw new Error( 45 | `Invalid transport; should be either 'ws', 'wrtc' or 'auto'.` 46 | ) 47 | } 48 | } 49 | } 50 | 51 | // isSupported returns whether the default protocol works reliably in the 52 | // current browser. Currently, the default protocol is WebSocket and is only 53 | // supported in H.264 capable browsers. You can use the experimental `auto` 54 | // protocol that switches to WebRTC in case H.264 is not available and ignore 55 | // this function. WebRTC does not work reliably, do not use in production. 56 | export function isSupported(): boolean { 57 | const mimeType = getMimeType() 58 | const supported = mimeType.includes('h264') 59 | return supported 60 | } 61 | 62 | export { CastSession } from './CastSession' 63 | export { WebSocketError } from './websocket' 64 | -------------------------------------------------------------------------------- /src/web.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '.' 2 | 3 | globalThis.webRTMP = { Client } 4 | -------------------------------------------------------------------------------- /src/webrtc.ts: -------------------------------------------------------------------------------- 1 | import { CastSession } from './CastSession' 2 | 3 | async function iceHandshake( 4 | secure: boolean, 5 | baseUrl: string, 6 | streamKey: string, 7 | localDesc: RTCSessionDescription 8 | ) { 9 | const protocol = secure ? 'https' : 'http' 10 | 11 | const qsKey = streamKey.includes('://') ? 'rtmp' : 'streamKey' 12 | 13 | const answer = await fetch( 14 | `${protocol}://${baseUrl}/wrtc/offer?${qsKey}=${streamKey}`, 15 | { 16 | method: 'POST', 17 | body: JSON.stringify(localDesc), 18 | headers: { 19 | ['content-type']: 'application/json', 20 | }, 21 | } 22 | ) 23 | 24 | if (answer.status !== 200) { 25 | throw new Error(`Error response from server: ${answer.status}`) 26 | } 27 | 28 | const sessionInit = await answer.json() 29 | 30 | return new RTCSessionDescription(sessionInit) 31 | } 32 | 33 | function castViaWebRTC( 34 | secure: boolean, 35 | baseUrl: string, 36 | stream: MediaStream, 37 | streamKey: string 38 | ): CastSession { 39 | const pc = new RTCPeerConnection({ 40 | iceServers: [ 41 | { 42 | urls: 'stun:stun.l.google.com:19302', 43 | }, 44 | ], 45 | }) 46 | 47 | const cast = new CastSession(() => pc.close()) 48 | 49 | stream.getTracks().forEach((track) => pc.addTrack(track, stream)) 50 | 51 | // pc.oniceconnectionstatechange = () => {} 52 | 53 | pc.onicegatheringstatechange = async () => { 54 | if (pc.iceGatheringState !== 'complete') { 55 | return 56 | } 57 | 58 | try { 59 | const remoteDesc = await iceHandshake( 60 | secure, 61 | baseUrl, 62 | streamKey, 63 | pc.localDescription 64 | ) 65 | 66 | if (pc.signalingState !== 'closed') { 67 | await pc.setRemoteDescription(remoteDesc) 68 | } 69 | } catch (err) { 70 | cast.emit('error', err) 71 | } 72 | } 73 | 74 | pc.onconnectionstatechange = () => { 75 | const state = pc.connectionState 76 | 77 | switch (state) { 78 | case 'connected': 79 | cast.emit('open') 80 | break 81 | case 'closed': 82 | cast.emit('closed') 83 | break 84 | case 'failed': 85 | cast.emit('error', new Error('WebRTC connection failed.')) 86 | break 87 | } 88 | } 89 | 90 | const initPeerConn = async () => { 91 | try { 92 | const offer = await pc.createOffer() 93 | await pc.setLocalDescription(offer) 94 | } catch (err) { 95 | cast.emit('error', err) 96 | } 97 | } 98 | 99 | initPeerConn() 100 | 101 | return cast 102 | } 103 | 104 | export default castViaWebRTC 105 | -------------------------------------------------------------------------------- /src/websocket.ts: -------------------------------------------------------------------------------- 1 | import { CastSession } from './CastSession' 2 | 3 | export class WebSocketError extends Error { 4 | constructor(public code: number, message?: string) { 5 | super(message) 6 | } 7 | } 8 | 9 | export function getMimeType(): string | null { 10 | const types = [ 11 | 'video/webm;codecs=h264', 12 | 'video/webm', 13 | 'video/webm;codecs=opus', 14 | 'video/webm;codecs=vp8', 15 | 'video/webm;codecs=daala', 16 | 'video/mpeg', 17 | 'video/mp4', 18 | ] 19 | 20 | let mimeType: string | null = null 21 | for (const type of types) { 22 | const supported = MediaRecorder.isTypeSupported(type) 23 | if (supported) { 24 | mimeType = type 25 | break 26 | } 27 | } 28 | 29 | return mimeType 30 | } 31 | 32 | function querystring(params: Record) { 33 | const escape = encodeURIComponent 34 | const raw = Object.keys(params) 35 | .filter((k) => !!params[k]) 36 | .map((k) => escape(k) + '=' + escape(params[k].toString())) 37 | .join('&') 38 | return raw.length === 0 ? '' : '?' + raw 39 | } 40 | 41 | function connect( 42 | secure: boolean, 43 | baseUrl: string, 44 | streamKey: string, 45 | mimeType: string 46 | ) { 47 | const protocol = secure ? 'wss' : 'ws' 48 | 49 | const qsKey = streamKey.includes('://') ? 'rtmp' : 'streamKey' 50 | 51 | const query = querystring({ [qsKey]: streamKey, mimeType }) 52 | 53 | const url = `${protocol}://${baseUrl}/ws${query}` 54 | 55 | const socket = new WebSocket(url) 56 | 57 | return socket 58 | } 59 | 60 | function stop_recording(media_recorder: MediaRecorder, socket: WebSocket) { 61 | stop_media_recorder(media_recorder) 62 | 63 | socket.close(1000) 64 | } 65 | 66 | const MEDIA_RECORDER_T = 2000 67 | 68 | function start_media_recorder(media_recorder: MediaRecorder): void { 69 | if (media_recorder.state === 'recording') { 70 | return 71 | } 72 | media_recorder.start(MEDIA_RECORDER_T) 73 | } 74 | 75 | function stop_media_recorder(media_recorder: MediaRecorder): void { 76 | if (media_recorder.state === 'inactive') { 77 | return 78 | } 79 | media_recorder.ondataavailable = null 80 | media_recorder.stop() 81 | } 82 | 83 | function castViaWebSocket( 84 | secure: boolean, 85 | baseUrl: string, 86 | stream: MediaStream, 87 | streamKey: string 88 | ): CastSession { 89 | if (!window.MediaRecorder) { 90 | throw new Error('Media Recorder API is not supported in this browser.') 91 | } 92 | 93 | const mimeType = getMimeType() 94 | 95 | if (!mimeType) { 96 | throw new Error('Media Recorder does not support any valid MIME type.') 97 | } 98 | 99 | const socket = connect(secure, baseUrl, streamKey, mimeType) 100 | 101 | const recorder = new MediaRecorder(stream, { 102 | mimeType, 103 | audioBitsPerSecond: 128 * 1000, 104 | videoBitsPerSecond: 3 * 1024 * 1024, 105 | }) 106 | 107 | const cast = new CastSession(() => stop_recording(recorder, socket)) 108 | 109 | let connected = false 110 | 111 | recorder.ondataavailable = function (event) { 112 | const { data } = event 113 | 114 | if (connected) { 115 | socket.send(data) 116 | } 117 | } 118 | 119 | socket.addEventListener('open', () => { 120 | connected = true 121 | 122 | start_media_recorder(recorder) 123 | 124 | cast.emit('open') 125 | }) 126 | 127 | socket.addEventListener('close', ({ code, reason }) => { 128 | connected = false 129 | 130 | stop_recording(recorder, socket) 131 | 132 | if (code !== 1000) { 133 | cast.emit('error', new WebSocketError(code, 'abnormal websocket closure')) 134 | } 135 | 136 | cast.emit('close') 137 | }) 138 | 139 | return cast 140 | } 141 | 142 | export default castViaWebSocket 143 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "lib": ["es2015", "es2016", "es2017", "dom"], 5 | "outDir": "lib", 6 | "allowJs": false, 7 | "sourceRoot": "src", 8 | "sourceMap": true, 9 | "target": "es2015", 10 | "module": "commonjs", 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": false, 13 | "declaration": true, 14 | "typeRoots": ["node_modules/@types"], 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true 17 | }, 18 | "typeAcquisition": { 19 | "enable": false 20 | }, 21 | "include": ["./src/**/*.ts"], 22 | "exclude": ["node_modules", "lib", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/events@^3.0.0": 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" 8 | integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== 9 | 10 | esbuild@0.12.28: 11 | version "0.12.28" 12 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.28.tgz#84da0d2a0d0dee181281545271e0d65cf6fab1ef" 13 | integrity sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA== 14 | 15 | events@3.3.0: 16 | version "3.3.0" 17 | resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" 18 | integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== 19 | 20 | prettier@2.4.0: 21 | version "2.4.0" 22 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.0.tgz#85bdfe0f70c3e777cf13a4ffff39713ca6f64cba" 23 | integrity sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ== 24 | 25 | typescript@4.4.3: 26 | version "4.4.3" 27 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" 28 | integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== 29 | --------------------------------------------------------------------------------