├── Documentation ├── WebInstallation.md └── ChromeOSInstallation.md ├── MediaStream.js ├── RTCView ├── resolveAssetSource.js ├── RTCViewResizeMode.js ├── __tests__ │ └── index-test.js └── index.js ├── RTCIceCandidate.js ├── getUserMedia.js ├── MediaStreamTrack.js ├── RTCPeerConnection.js ├── RTCSessionDescription.js ├── .gitignore ├── index.js ├── .travis.yml ├── package.json ├── LICENSE └── README.md /Documentation/WebInstallation.md: -------------------------------------------------------------------------------- 1 | ## Web Installation 2 | 3 | TODO: -------------------------------------------------------------------------------- /Documentation/ChromeOSInstallation.md: -------------------------------------------------------------------------------- 1 | ## ChromeOS Installation 2 | 3 | TODO: -------------------------------------------------------------------------------- /MediaStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var MediaStream = window.MediaStream || window.mozMediaStream || 3 | window.webkitMediaStream || window.msMediaStream 4 | 5 | export default MediaStream -------------------------------------------------------------------------------- /RTCView/resolveAssetSource.js: -------------------------------------------------------------------------------- 1 | function resolveAssetSource(source) { 2 | return ((typeof source === 'object') ? source.uri : source) || null; 3 | } 4 | 5 | module.exports = resolveAssetSource; 6 | -------------------------------------------------------------------------------- /RTCIceCandidate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || 3 | window.webkitRTCIceCandidate || window.msRTCIceCandidate 4 | 5 | export default RTCIceCandidate -------------------------------------------------------------------------------- /getUserMedia.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getUserMedia = (navigator.getUserMedia || navigator.mozGetUserMedia || 4 | navigator.webkitGetUserMedia || navigator.msGetUserMedia).bind(navigator); 5 | export default getUserMedia; 6 | 7 | -------------------------------------------------------------------------------- /MediaStreamTrack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'use strict'; 3 | var MediaStreamTrack = window.MediaStreamTrack || window.mozMediaStreamTrack || 4 | window.webkitMediaStreamTrack || window.msMediaStreamTrack 5 | 6 | export default MediaStreamTrack -------------------------------------------------------------------------------- /RTCPeerConnection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || 4 | window.webkitRTCPeerConnection || window.msRTCPeerConnection 5 | 6 | export default RTCPeerConnection 7 | 8 | -------------------------------------------------------------------------------- /RTCSessionDescription.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || 4 | window.webkitRTCSessionDescription || window.msRTCSessionDescription 5 | 6 | export default RTCSessionDescription 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 3 | node_modules 4 | ios/RCTWebRTC.xcodeproj/xcuserdata 5 | ios/RCTWebRTC.xcodeproj/project.xcworkspace 6 | .DS_Store 7 | .idea 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import RTCPeerConnection from './RTCPeerConnection'; 4 | import RTCIceCandidate from './RTCIceCandidate'; 5 | import RTCSessionDescription from './RTCSessionDescription'; 6 | import MediaStream from './MediaStream'; 7 | import MediaStreamTrack from './MediaStreamTrack'; 8 | import getUserMedia from './getUserMedia'; 9 | import RTCView from './RTCView'; 10 | 11 | module.exports = { 12 | RTCPeerConnection, 13 | RTCIceCandidate, 14 | RTCSessionDescription, 15 | MediaStream, 16 | MediaStreamTrack, 17 | getUserMedia, 18 | RTCView, 19 | }; 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.2.2 4 | after_script: 5 | - git describe --tags --exact-match 2>/dev/null 6 | - echo $? 7 | notifications: 8 | slack: liiveteam:Aj7V8m8n5p2GGf3F4cHTiv3W 9 | branches: 10 | only: 11 | - master 12 | - develop 13 | on_success: change 14 | on_failure: always 15 | on_start: never 16 | deploy: 17 | provider: npm 18 | api_key: $NPM_KEY 19 | email: "robblovell@gmail.com" 20 | on: 21 | tags: true 22 | branch: master 23 | skip_cleanup: true 24 | clean_up: false 25 | repo: liivevideo/react-native-web-webrtc 26 | 27 | -------------------------------------------------------------------------------- /RTCView/RTCViewResizeMode.js: -------------------------------------------------------------------------------- 1 | 2 | var keyMirror = function(obj) { 3 | var ret = {}; 4 | var key; 5 | if (!(obj instanceof Object && !Array.isArray(obj))) { 6 | throw new Error('keyMirror(...): Argument must be an object.'); 7 | } 8 | for (key in obj) { 9 | if (obj.hasOwnProperty(key)) { 10 | ret[key] = key; 11 | } 12 | } 13 | return ret; 14 | }; 15 | 16 | const RTCViewResizeMode = keyMirror({ 17 | center: null, 18 | contain: null, 19 | cover: null, 20 | none: null, 21 | repeat: null, 22 | stretch: null 23 | }); 24 | 25 | module.exports = RTCViewResizeMode; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-web-webrtc", 3 | "description": "A sister project to react-native-web and react-native-webrtc that allows webrtc to be used with react-native-web projects.", 4 | "version": "0.0.7", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/liivevideo/react-native-web-webrtc.git" 8 | }, 9 | "scripts": { 10 | "test": "echo 'no tests'" 11 | }, 12 | "nativePackage": false, 13 | "author": { 14 | "name": "Robb", 15 | "email": "robblovell@gmail.com" 16 | }, 17 | "homepage": "https://github.com/liivevideo/react-native-web-webrtc", 18 | "keywords": [ 19 | "react-component", 20 | "react-native", 21 | "react-native-web", 22 | "chromeOS", 23 | "browsers", 24 | "web", 25 | "webrtc" 26 | ], 27 | "dependencies": { 28 | "react-native-web": "https://github.com/liivevideo/react-native-web.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/liivevideo/react-native-web-webrtc/issues" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 LiiveVideo 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 | # react-native-web-webrtc 2 | 3 | A WebRTC module for React Native Web. 4 | 5 | [![Build Status](https://travis-ci.org/liivevideo/react-native-web-webrtc.svg?branch=master)](https://travis-ci.org/liivevideo/react-native-web-webrtc) 6 | 7 | ## Support 8 | - Supports browsers and chromeOS. 9 | - Support video and audio communication. 10 | - Supports data channels. 11 | - You can use it to build a client side application using react-native-web that can use WebRTC. 12 | 13 | ## Installation 14 | 15 | ### react-native-web-webrtc: 16 | 17 | - [Web](https://github.com/liivevideo/react-native-web-webrtc/blob/master/Documentation/WebInstallation.md) 18 | - [ChromeOS](https://github.com/liivevideo/react-native-web-webrtc/blob/master/Documentation/ChromeOSInstallation.md) 19 | 20 | ## Usage 21 | Now, you can use WebRTC using react-native in a browser or chromeOS. 22 | 23 | In your `index.web.js`/`index.chromeos.js` file: 24 | 25 | ```javascript 26 | var WebRTC = require('react-native-web-webrtc'); 27 | var { 28 | RTCPeerConnection, 29 | RTCMediaStream, 30 | RTCIceCandidate, 31 | RTCSessionDescription, 32 | MediaStreamTrack, 33 | getUserMedia, 34 | RTCView 35 | } = WebRTC; 36 | ``` 37 | 38 | ## TODO: 39 | 40 | * Example project 41 | * Documentation of implementation 42 | * Clean up RTCView: unnecessary code. 43 | 44 | ## Sister Projects: 45 | 46 | This is a sister project to: [react-native-webrtc](https://github.com/oney/react-native-webrtc) and [react-native-web](git://github.com/necolas/react-native-web.git) 47 | -------------------------------------------------------------------------------- /RTCView/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import RTCView from '../'; 5 | import React from 'react'; 6 | import StyleSheet from '../../../apis/StyleSheet'; 7 | import { mount, shallow } from 'enzyme'; 8 | 9 | suite('components/RTCView', () => { 10 | test('sets correct accessibility role"', () => { 11 | const rtcVideoView = shallow(); 12 | assert.equal(rtcVideoView.prop('accessibilityRole'), 'video'); 13 | }); 14 | 15 | test('prop "accessibilityLabel"', () => { 16 | const accessibilityLabel = 'accessibilityLabel'; 17 | const rtcVideoView = shallow(); 18 | assert.equal(rtcVideoView.prop('accessibilityLabel'), accessibilityLabel); 19 | }); 20 | 21 | test('prop "accessible"', () => { 22 | const accessible = false; 23 | const rtcVideoView = shallow(); 24 | assert.equal(rtcVideoView.prop('accessible'), accessible); 25 | }); 26 | 27 | test('prop "children"', () => { 28 | const children =
; 29 | const wrapper = shallow({children}); 30 | assert.equal(wrapper.contains(children), true); 31 | }); 32 | 33 | suite('prop "defaultSource"', () => { 34 | test('sets background rtcVideoView when value is an object', () => { 35 | const defaultSource = { uri: 'https://google.com/favicon.ico' }; 36 | const rtcVideoView = shallow(); 37 | const backgroundRTCView = StyleSheet.flatten(rtcVideoView.prop('style')).backgroundRTCView; 38 | assert(backgroundRTCView.indexOf(defaultSource.uri) > -1); 39 | }); 40 | 41 | test('sets background rtcVideoView when value is a string', () => { 42 | // emulate require-ed asset 43 | const defaultSource = 'https://google.com/favicon.ico'; 44 | const rtcVideoView = shallow(); 45 | const backgroundRTCView = StyleSheet.flatten(rtcVideoView.prop('style')).backgroundRTCView; 46 | assert(backgroundRTCView.indexOf(defaultSource) > -1); 47 | }); 48 | }); 49 | 50 | test('prop "onError"', function (done) { 51 | this.timeout(5000); 52 | mount(); 53 | function onError(e) { 54 | assert.equal(e.nativeEvent.type, 'error'); 55 | done(); 56 | } 57 | }); 58 | 59 | test('prop "onLoad"', function (done) { 60 | this.timeout(5000); 61 | const rtcVideoView = mount(); 62 | function onLoad(e) { 63 | assert.equal(e.nativeEvent.type, 'load'); 64 | const hasBackgroundRTCView = (rtcVideoView.html()).indexOf('url("https://google.com/favicon.ico")') > -1; 65 | assert.equal(hasBackgroundRTCView, true); 66 | done(); 67 | } 68 | }); 69 | 70 | test('prop "onLoadEnd"', function (done) { 71 | this.timeout(5000); 72 | const rtcVideoView = mount(); 73 | function onLoadEnd() { 74 | assert.ok(true); 75 | const hasBackgroundRTCView = (rtcVideoView.html()).indexOf('url("https://google.com/favicon.ico")') > -1; 76 | assert.equal(hasBackgroundRTCView, true); 77 | done(); 78 | } 79 | }); 80 | 81 | test('prop "onLoadStart"', function (done) { 82 | this.timeout(5000); 83 | mount(); 84 | function onLoadStart() { 85 | assert.ok(true); 86 | done(); 87 | } 88 | }); 89 | 90 | suite('prop "resizeMode"', () => { 91 | const getBackgroundSize = (rtcVideoView) => StyleSheet.flatten(rtcVideoView.prop('style')).backgroundSize; 92 | 93 | test('value "contain"', () => { 94 | const rtcVideoView = shallow(); 95 | assert.equal(getBackgroundSize(rtcVideoView), 'contain'); 96 | }); 97 | 98 | test('value "cover"', () => { 99 | const rtcVideoView = shallow(); 100 | assert.equal(getBackgroundSize(rtcVideoView), 'cover'); 101 | }); 102 | 103 | test('value "none"', () => { 104 | const rtcVideoView = shallow(); 105 | assert.equal(getBackgroundSize(rtcVideoView), 'auto'); 106 | }); 107 | 108 | test('value "stretch"', () => { 109 | const rtcVideoView = shallow(); 110 | assert.equal(getBackgroundSize(rtcVideoView), '100% 100%'); 111 | }); 112 | 113 | test('no value', () => { 114 | const rtcVideoView = shallow(); 115 | assert.equal(getBackgroundSize(rtcVideoView), 'cover'); 116 | }); 117 | }); 118 | 119 | suite('prop "source"', function () { 120 | this.timeout(5000); 121 | 122 | test('sets background rtcVideoView when value is an object', (done) => { 123 | const source = { uri: 'https://google.com/favicon.ico' }; 124 | mount(); 125 | function onLoad(e) { 126 | const src = e.nativeEvent.target.src; 127 | assert.equal(src, source.uri); 128 | done(); 129 | } 130 | }); 131 | 132 | test('sets background rtcVideoView when value is a string', (done) => { 133 | // emulate require-ed asset 134 | const source = 'https://google.com/favicon.ico'; 135 | mount(); 136 | function onLoad(e) { 137 | const src = e.nativeEvent.target.src; 138 | assert.equal(src, source); 139 | done(); 140 | } 141 | }); 142 | }); 143 | 144 | suite('prop "style"', () => { 145 | test('converts "resizeMode" property', () => { 146 | const rtcVideoView = shallow(); 147 | assert.equal(StyleSheet.flatten(rtcVideoView.prop('style')).backgroundSize, 'contain'); 148 | }); 149 | 150 | test('removes "resizeMode" property', () => { 151 | const rtcVideoView = shallow(); 152 | assert.equal(StyleSheet.flatten(rtcVideoView.prop('style')).resizeMode, undefined); 153 | }); 154 | }); 155 | 156 | test('prop "testID"', () => { 157 | const testID = 'testID'; 158 | const rtcVideoView = shallow(); 159 | assert.equal(rtcVideoView.prop('testID'), testID); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /RTCView/index.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | 3 | import resolveAssetSource from './resolveAssetSource'; 4 | import RTCViewResizeMode from './RTCViewResizeMode'; 5 | 6 | import { 7 | createDOMElement, 8 | StyleSheet, 9 | View 10 | } from 'react-native-web' 11 | 12 | import React, { Component, PropTypes } from 'react'; 13 | 14 | const STATUS_ERRORED = 'ERRORED'; 15 | const STATUS_LOADED = 'LOADED'; 16 | const STATUS_LOADING = 'LOADING'; 17 | const STATUS_PENDING = 'PENDING'; 18 | const STATUS_IDLE = 'IDLE'; 19 | 20 | const RTCViewSourcePropType = PropTypes.oneOfType([ 21 | PropTypes.shape({ 22 | uri: PropTypes.string.isRequired 23 | }), 24 | PropTypes.string 25 | ]); 26 | 27 | class RTCView extends Component { 28 | static displayName = 'RTCView' 29 | 30 | static propTypes = { 31 | ...View.propTypes, 32 | width: PropTypes.number, 33 | height: PropTypes.number, 34 | streamURL: PropTypes.string, 35 | muted: PropTypes.string, 36 | autoPlay: PropTypes.string, 37 | children: PropTypes.any, 38 | defaultSource: RTCViewSourcePropType, 39 | onError: PropTypes.func, 40 | onLayout: PropTypes.func, 41 | onLoad: PropTypes.func, 42 | onLoadEnd: PropTypes.func, 43 | onLoadStart: PropTypes.func, 44 | source: RTCViewSourcePropType, 45 | }; 46 | 47 | static defaultProps = {}; 48 | 49 | static resizeMode = RTCViewResizeMode; 50 | 51 | constructor(props, context) { 52 | super(props, context); 53 | const uri = resolveAssetSource(props.source); 54 | this._rtcVideoViewState = uri ? STATUS_PENDING : STATUS_IDLE; 55 | this.state = { isLoaded: false }; 56 | } 57 | 58 | componentDidMount() { 59 | if (this._rtcVideoViewState === STATUS_PENDING) { 60 | this._createRTCViewLoader(); 61 | } 62 | } 63 | 64 | componentDidUpdate() { 65 | if (this._rtcVideoViewState === STATUS_PENDING && !this.rtcVideoView) { 66 | this._createRTCViewLoader(); 67 | } 68 | } 69 | 70 | componentWillReceiveProps(nextProps) { 71 | const nextUri = resolveAssetSource(nextProps.source); 72 | if (resolveAssetSource(this.props.source) !== nextUri) { 73 | this._updateRTCViewState(nextUri ? STATUS_PENDING : STATUS_IDLE); 74 | } 75 | } 76 | 77 | componentWillUnmount() { 78 | this._destroyRTCViewLoader(); 79 | } 80 | 81 | render() { 82 | const { isLoaded } = this.state; 83 | const { 84 | streamURL, 85 | autoPlay, 86 | muted, 87 | accessibilityLabel, 88 | accessible, 89 | children, 90 | defaultSource, 91 | onLayout, 92 | source, 93 | testID 94 | } = this.props; 95 | 96 | const displayRTCView = resolveAssetSource(!isLoaded ? defaultSource : source); 97 | const backgroundRTCView = displayRTCView ? `url("${displayRTCView}")` : null; 98 | let style = StyleSheet.flatten(this.props.style); 99 | 100 | const resizeMode = this.props.resizeMode || style.resizeMode || RTCViewResizeMode.cover; 101 | // remove 'resizeMode' style, as it is not supported by View (N.B. styles are frozen in dev) 102 | style = process.env.NODE_ENV !== 'production' ? { ...style } : style; 103 | delete style.resizeMode; 104 | 105 | /** 106 | * RTCView is a non-stretching View. The rtcVideoView is displayed as a background 107 | * rtcVideoView to support `resizeMode`. The HTML rtcVideoView is hidden but used to 108 | * provide the correct responsive rtcVideoView dimensions, and to support the 109 | * rtcVideoView context menu. Child content is rendered into an element absolutely 110 | * positioned over the rtcVideoView. 111 | */ 112 | var attributes = {src: streamURL} 113 | if (muted != null && muted != undefined && muted) 114 | attributes.muted = 'muted' 115 | if (autoPlay != null && autoPlay != undefined && autoPlay) 116 | attributes.autoPlay = 'autoPlay' 117 | 118 | return ( 119 | 132 | {createDOMElement('video', attributes)} 133 | {children ? ( 134 | 135 | ) : null} 136 | 137 | ); 138 | } 139 | 140 | _createRTCViewLoader() { 141 | const uri = resolveAssetSource(this.props.source); 142 | 143 | this._destroyRTCViewLoader(); 144 | this.rtcVideoView = new window.RTCView(); 145 | this.rtcVideoView.onerror = this._onError; 146 | this.rtcVideoView.onload = this._onLoad; 147 | this.rtcVideoView.src = uri; 148 | this._onLoadStart(); 149 | } 150 | 151 | _destroyRTCViewLoader() { 152 | if (this.rtcVideoView) { 153 | this.rtcVideoView.onerror = null; 154 | this.rtcVideoView.onload = null; 155 | this.rtcVideoView = null; 156 | } 157 | } 158 | 159 | _onError = (e) => { 160 | const { onError } = this.props; 161 | const event = { nativeEvent: e }; 162 | 163 | this._destroyRTCViewLoader(); 164 | this._updateRTCViewState(STATUS_ERRORED); 165 | this._onLoadEnd(); 166 | if (onError) { onError(event); } 167 | }; 168 | 169 | _onLoad = (e) => { 170 | const { onLoad } = this.props; 171 | const event = { nativeEvent: e }; 172 | 173 | this._destroyRTCViewLoader(); 174 | this._updateRTCViewState(STATUS_LOADED); 175 | if (onLoad) { onLoad(event); } 176 | this._onLoadEnd(); 177 | }; 178 | 179 | _onLoadEnd() { 180 | const { onLoadEnd } = this.props; 181 | if (onLoadEnd) { onLoadEnd(); } 182 | } 183 | 184 | _onLoadStart() { 185 | const { onLoadStart } = this.props; 186 | this._updateRTCViewState(STATUS_LOADING); 187 | if (onLoadStart) { onLoadStart(); } 188 | } 189 | 190 | _updateRTCViewState(status) { 191 | this._rtcVideoViewState = status; 192 | const isLoaded = this._rtcVideoViewState === STATUS_LOADED; 193 | if (isLoaded !== this.state.isLoaded) { 194 | this.setState({ isLoaded }); 195 | } 196 | } 197 | } 198 | 199 | const styles = StyleSheet.create({ 200 | initial: { 201 | alignSelf: 'flex-start', 202 | backgroundColor: 'transparent', 203 | backgroundPosition: 'center', 204 | backgroundRepeat: 'no-repeat', 205 | backgroundSize: 'cover' 206 | }, 207 | video: { 208 | borderWidth: 0, 209 | height: 'auto', 210 | maxHeight: '100%', 211 | maxWidth: '100%', 212 | opacity: 0 213 | }, 214 | children: { 215 | bottom: 0, 216 | left: 0, 217 | position: 'absolute', 218 | right: 0, 219 | top: 0 220 | } 221 | }); 222 | 223 | const resizeModeStyles = StyleSheet.create({ 224 | center: { 225 | backgroundSize: 'auto', 226 | backgroundPosition: 'center' 227 | }, 228 | contain: { 229 | backgroundSize: 'contain' 230 | }, 231 | cover: { 232 | backgroundSize: 'cover' 233 | }, 234 | none: { 235 | backgroundSize: 'auto' 236 | }, 237 | repeat: { 238 | backgroundSize: 'auto', 239 | backgroundRepeat: 'repeat' 240 | }, 241 | stretch: { 242 | backgroundSize: '100% 100%' 243 | } 244 | }); 245 | 246 | module.exports = RTCView; 247 | 248 | --------------------------------------------------------------------------------