├── .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 [](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 |
63 |
66 |
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 |
--------------------------------------------------------------------------------