├── .gitignore ├── plugins ├── audiobridge │ ├── AudioBridgeParticipant.js │ └── JanusAudioBridgePlugin.js ├── videoroom │ ├── JanusVideoRoomPublisher.js │ └── JanusVideoRoomPlugin.js └── streaming │ └── JanusStreamingPlugin.js ├── index.js ├── utils ├── iceServers.js ├── JanusUtils.js ├── JanusSocket.js └── JanusPlugin.js ├── package.json ├── Janus.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /plugins/audiobridge/AudioBridgeParticipant.js: -------------------------------------------------------------------------------- 1 | export default class JanusAudioBridgeParticipant { 2 | constructor({display, id, talking, muted, setup}) { 3 | this.id = id; 4 | this.displayName = display; 5 | this.isTalking = talking; 6 | this.isMuted = muted; 7 | this.didSetup = setup; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Janus from './Janus'; 2 | import JanusVideoRoomPlugin from './plugins/videoroom/JanusVideoRoomPlugin'; 3 | import JanusStreamingPlugin from './plugins/streaming/JanusStreamingPlugin'; 4 | import JanusVideoRoomPublisher from './plugins/videoroom/JanusVideoRoomPublisher'; 5 | 6 | export {Janus, JanusVideoRoomPlugin, JanusStreamingPlugin, JanusVideoRoomPublisher}; 7 | -------------------------------------------------------------------------------- /plugins/videoroom/JanusVideoRoomPublisher.js: -------------------------------------------------------------------------------- 1 | export default class JanusVideoRoomPublisher { 2 | constructor({audio_codec, display, id, talking, video_codec}) { 3 | this.id = id; 4 | this.displayName = display; 5 | this.isTalking = talking; 6 | this.codecs = { 7 | audio: audio_codec, 8 | video: video_codec, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /utils/iceServers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'iceServers': [ 3 | { 4 | 'url': 'stun:stun.l.google.com:19302', 5 | }, 6 | { 7 | 'url': 'stun:stun1.l.google.com:19302', 8 | }, 9 | { 10 | 'url': 'stun:stun2.l.google.com:19302', 11 | }, 12 | { 13 | 'url': 'stun:stun3.l.google.com:19302', 14 | }, 15 | { 16 | 'url': 'stun:stun4.l.google.com:19302', 17 | }, 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-janus", 3 | "version": "0.0.1", 4 | "description": "A react-native client for Janus WebRTC Gateway", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/canozgen9/react-native-janus.git" 12 | }, 13 | "keywords": [ 14 | "janus", 15 | "react-native" 16 | ], 17 | "author": "Can ÖZGEN", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/canozgen9/react-native-janus/issues" 21 | }, 22 | "homepage": "https://github.com/canozgen9/react-native-janus#readme" 23 | } 24 | -------------------------------------------------------------------------------- /utils/JanusUtils.js: -------------------------------------------------------------------------------- 1 | class JanusUtils { 2 | static log(...msg) { 3 | console.log('[Janus]:' + msg.map(msg => { 4 | if (typeof msg === 'object') { 5 | return JSON.stringify(msg); 6 | } 7 | return msg; 8 | }).join(', ')); 9 | } 10 | 11 | static randomString(length) { 12 | const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 13 | let randomString = ''; 14 | for (let i = 0; i < length; i++) { 15 | const randomPoz = Math.floor(Math.random() * charSet.length); 16 | randomString += charSet.substring(randomPoz, randomPoz + 1); 17 | } 18 | return randomString; 19 | } 20 | } 21 | 22 | export default JanusUtils; 23 | -------------------------------------------------------------------------------- /Janus.js: -------------------------------------------------------------------------------- 1 | import JanusSocket from './utils/JanusSocket'; 2 | 3 | export default class Janus { 4 | /** 5 | * @type {RTCSessionDescription} 6 | */ 7 | static RTCSessionDescription; 8 | /** 9 | * @type {RTCPeerConnection} 10 | */ 11 | static RTCPeerConnection; 12 | /** 13 | * @type {RTCIceCandidate} 14 | */ 15 | static RTCIceCandidate; 16 | 17 | apiSecret = null; 18 | iceServers = []; 19 | 20 | constructor(address) { 21 | this.socket = new JanusSocket(address); 22 | } 23 | 24 | /** 25 | * 26 | * @param RTCSessionDescription 27 | * @param RTCPeerConnection 28 | * @param RTCIceCandidate 29 | * @param MediaStream 30 | */ 31 | static setDependencies = ({RTCSessionDescription, RTCPeerConnection, RTCIceCandidate, MediaStream}) => { 32 | Janus.RTCSessionDescription = RTCSessionDescription; 33 | Janus.RTCPeerConnection = RTCPeerConnection; 34 | Janus.RTCIceCandidate = RTCIceCandidate; 35 | Janus.MediaStream = MediaStream; 36 | }; 37 | 38 | init = async () => { 39 | await this.socket.connect(); 40 | }; 41 | 42 | destroy = async () => { 43 | try { 44 | const destroySessionResponse = await this.socket.sendAsync({ 45 | 'janus': 'destroy', 46 | 'session_id': this.socket.sessionID, 47 | }); 48 | } catch (e) { 49 | console.error('destroy janus', e); 50 | } 51 | 52 | try { 53 | await this.socket.disconnect(); 54 | } catch (e) { 55 | console.error('destroy socket', e); 56 | } 57 | }; 58 | 59 | /** 60 | * 61 | * @param secret 62 | */ 63 | setApiSecret = (secret) => { 64 | this.apiSecret = secret; 65 | }; 66 | 67 | /** 68 | * 69 | * @param {Object[]} iceServers 70 | */ 71 | setIceServers = (iceServers) => { 72 | this.iceServers = iceServers; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Video Room 2 | 3 | ```javascript 4 | import {mediaDevices, MediaStream, RTCIceCandidate, RTCPeerConnection, RTCSessionDescription, RTCView} from 'react-native-webrtc'; 5 | import React from 'react'; 6 | import {Dimensions, FlatList, StatusBar, View} from 'react-native'; 7 | import {Janus, JanusVideoRoomPlugin} from 'react-native-janus'; 8 | 9 | Janus.setDependencies({ 10 | RTCPeerConnection, 11 | RTCSessionDescription, 12 | RTCIceCandidate, 13 | MediaStream, 14 | }); 15 | 16 | class JanusVideoRoomScreen extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | stream: null, 22 | publishers: [], 23 | }; 24 | } 25 | 26 | async receivePublisher(publisher) { 27 | try { 28 | let videoRoom = new JanusVideoRoomPlugin(this.janus); 29 | videoRoom.setRoomID(1234); 30 | videoRoom.setOnStreamListener((stream) => { 31 | this.setState(state => ({ 32 | publishers: [ 33 | ...state.publishers, 34 | { 35 | publisher: publisher, 36 | stream: stream, 37 | }, 38 | ], 39 | })); 40 | }); 41 | 42 | await videoRoom.createPeer(); 43 | await videoRoom.connect(); 44 | await videoRoom.receive(this.videoRoom.getUserPrivateID(), publisher); 45 | } catch (e) { 46 | console.error(e); 47 | } 48 | } 49 | 50 | async removePublisher(publisherID) { 51 | try { 52 | this.setState(state => ({ 53 | publishers: state.publishers.filter(pub => pub.publisher == null || pub.publisher.id !== publisherID), 54 | })); 55 | } catch (e) { 56 | console.error(e); 57 | } 58 | } 59 | 60 | async initJanus(stream) { 61 | try { 62 | this.setState(state => ({ 63 | publishers: [ 64 | { 65 | publisher: null, 66 | stream: stream, 67 | }, 68 | ], 69 | })); 70 | 71 | this.janus = new Janus('ws://${YOUR_JANUS_ADDRESS]:8188'); 72 | this.janus.setApiSecret('janusrocks'); 73 | await this.janus.init(); 74 | 75 | this.videoRoom = new JanusVideoRoomPlugin(this.janus); 76 | this.videoRoom.setRoomID(1234); 77 | this.videoRoom.setDisplayName('can'); 78 | this.videoRoom.setOnPublishersListener((publishers) => { 79 | for (let i = 0; i < publishers.length; i++) { 80 | this.receivePublisher(publishers[i]); 81 | } 82 | }); 83 | this.videoRoom.setOnPublisherJoinedListener((publisher) => { 84 | this.receivePublisher(publisher); 85 | }); 86 | this.videoRoom.setOnPublisherLeftListener((publisherID) => { 87 | this.removePublisher(publisherID); 88 | }); 89 | this.videoRoom.setOnWebRTCUpListener(async () => { 90 | 91 | }); 92 | 93 | await this.videoRoom.createPeer(); 94 | await this.videoRoom.connect(); 95 | await this.videoRoom.join(); 96 | await this.videoRoom.publish(stream); 97 | 98 | } catch (e) { 99 | console.error('main', JSON.stringify(e)); 100 | } 101 | } 102 | 103 | getMediaStream = async () => { 104 | let isFront = true; 105 | let sourceInfos = await mediaDevices.enumerateDevices(); 106 | let videoSourceId; 107 | for (let i = 0; i < sourceInfos.length; i++) { 108 | const sourceInfo = sourceInfos[i]; 109 | console.log(sourceInfo); 110 | if (sourceInfo.kind == 'videoinput' && sourceInfo.facing == (isFront ? 'front' : 'environment')) { 111 | videoSourceId = sourceInfo.deviceId; 112 | } 113 | } 114 | 115 | let stream = await mediaDevices.getUserMedia({ 116 | audio: true, 117 | video: { 118 | facingMode: (isFront ? 'user' : 'environment'), 119 | }, 120 | }); 121 | await this.initJanus(stream); 122 | }; 123 | 124 | async componentDidMount() { 125 | this.getMediaStream(); 126 | } 127 | 128 | componentWillUnmount = async () => { 129 | if (this.janus) { 130 | await this.janus.destroy(); 131 | } 132 | }; 133 | 134 | renderView() { 135 | } 136 | 137 | render() { 138 | return ( 139 | 140 | 141 | { 145 | if (item.publisher === null) { 146 | return `rtc-default`; 147 | } 148 | return `rtc-${item.publisher.id}`; 149 | }} 150 | renderItem={({item}) => ( 151 | 155 | )} 156 | /> 157 | 158 | ); 159 | } 160 | } 161 | 162 | export default JanusVideoRoomScreen; 163 | ``` 164 | -------------------------------------------------------------------------------- /utils/JanusSocket.js: -------------------------------------------------------------------------------- 1 | import JanusPlugin from './JanusPlugin'; 2 | import JanusUtils from './JanusUtils'; 3 | 4 | export default class JanusSocket { 5 | constructor(address) { 6 | this.address = address; 7 | this.transactions = {}; 8 | this.plugins = {}; 9 | this.senders = {}; 10 | this.sessionID = null; 11 | this.connected = false; 12 | this.keepAliveTimeoutID = null; 13 | } 14 | 15 | /** 16 | * 17 | * @param {JanusPlugin} plugin 18 | */ 19 | attachPlugin = (plugin) => { 20 | this.plugins[plugin.handleID] = plugin; 21 | }; 22 | 23 | /** 24 | * 25 | * @param {JanusPlugin} plugin 26 | */ 27 | detachPlugin = (plugin) => { 28 | delete this.plugins[plugin.handleID]; 29 | }; 30 | 31 | connect = () => { 32 | return new Promise(((resolve, reject) => { 33 | this.ws = new WebSocket(this.address, ['janus-protocol']); 34 | 35 | this.ws.onopen = () => { 36 | this.send({ 37 | 'janus': 'create', 38 | }, async (response) => { 39 | if (response.janus === 'success') { 40 | this.sessionID = response.data.id; 41 | this.connected = true; 42 | this.setKeepAliveTimeout(); 43 | resolve(); 44 | } else { 45 | //todo: send error 46 | reject(); 47 | } 48 | }); 49 | }; 50 | 51 | this.ws.onmessage = async (e) => { 52 | try { 53 | let message = JSON.parse(e.data); 54 | 55 | switch (message.janus) { 56 | // general events 57 | case 'keepalive': 58 | case 'ack': 59 | case 'timeout': { 60 | return await this.onMessage(message); 61 | } 62 | 63 | // plugin events 64 | case 'trickle': 65 | case 'webrtcup': 66 | case 'hangup': 67 | case 'detached': 68 | case 'media': 69 | case 'slowlink': 70 | case 'event': { 71 | if (message.transaction) { 72 | const {transaction} = message; 73 | await this.transactions[transaction](message); 74 | return delete this.transactions[transaction]; 75 | } else { 76 | return await this.plugins[message.sender].onMessage(message); 77 | } 78 | } 79 | 80 | // transaction responses 81 | case 'success': 82 | case 'error': { 83 | if (message.transaction) { 84 | const {transaction} = message; 85 | await this.transactions[transaction](message); 86 | delete this.transactions[transaction]; 87 | return; 88 | } 89 | } 90 | } 91 | 92 | if (message.janus && message.janus !== 'ack' && message.transaction) { 93 | const {transaction} = message; 94 | await this.transactions[transaction](message); 95 | delete this.transactions[transaction]; 96 | } else { 97 | if (message.sender && this.plugins[message.sender] && this.plugins[message.sender].onMessage) { 98 | await this.plugins[message.sender].onMessage(message); 99 | } else { 100 | await this.onMessage(message); 101 | } 102 | } 103 | } catch (e) { 104 | 105 | } 106 | }; 107 | 108 | this.ws.onerror = (e) => { 109 | JanusUtils.log('socket_error', e); 110 | }; 111 | 112 | this.ws.onclose = (e) => { 113 | JanusUtils.log('socket_closed', e.code, e.reason); 114 | }; 115 | })); 116 | }; 117 | 118 | disconnect = async () => { 119 | this.ws.close(); 120 | }; 121 | 122 | onMessage = async (message) => { 123 | 124 | }; 125 | 126 | send = (request, callback = null, transaction = null) => { 127 | if (!transaction && typeof callback === 'function') { 128 | transaction = JanusUtils.randomString(12); 129 | this.transactions[transaction] = callback; 130 | } 131 | 132 | request['transaction'] = transaction; 133 | 134 | this.ws.send(JSON.stringify(request)); 135 | }; 136 | 137 | sendAsync = (request) => { 138 | return new Promise((resolve, reject) => { 139 | this.send(request, (response) => { 140 | if (response.janus !== 'error') { 141 | resolve(response); 142 | } else { 143 | reject(response); 144 | } 145 | }); 146 | }); 147 | }; 148 | 149 | keepAlive = () => { 150 | if (this.connected) { 151 | this.setKeepAliveTimeout(); 152 | this.send({ 153 | 'janus': 'keepalive', 154 | 'session_id': this.sessionID, 155 | }, (response) => { 156 | // JanusUtils.log('keepAlive', response.session_id); 157 | }); 158 | } 159 | }; 160 | 161 | setKeepAliveTimeout = () => { 162 | if (this.connected) { 163 | this.keepAliveTimeoutID = setTimeout(this.keepAlive, 25000); 164 | } 165 | }; 166 | } 167 | -------------------------------------------------------------------------------- /utils/JanusPlugin.js: -------------------------------------------------------------------------------- 1 | import JanusUtils from './JanusUtils'; 2 | import Janus from './../Janus'; 3 | import iceServers from './iceServers'; 4 | 5 | class JanusPlugin { 6 | /** 7 | * 8 | * @callback onWebRTCUpListener 9 | */ 10 | /** 11 | * 12 | * @type {onWebRTCUpListener} 13 | */ 14 | onWebRTCUpListener = null; 15 | 16 | /** 17 | * 18 | * @callback onStreamListener 19 | */ 20 | /** 21 | * 22 | * @type {onStreamListener} 23 | */ 24 | onStreamListener = null; 25 | 26 | /** 27 | * 28 | * @type {boolean} 29 | */ 30 | isWebRtcUp = false; 31 | 32 | /** 33 | * @type {Janus.RTCPeerConnection} 34 | */ 35 | pc = null; 36 | 37 | /** 38 | * @type {Janus.RTCIceCandidate[]} 39 | */ 40 | cachedCandidates = []; 41 | 42 | /** 43 | * 44 | * @type {Janus} 45 | */ 46 | janus = null; 47 | 48 | /** 49 | * 50 | * @type {boolean} 51 | */ 52 | isReceivingAudio = false; 53 | 54 | 55 | /** 56 | * 57 | * @type {boolean} 58 | */ 59 | isReceivingVideo = false; 60 | 61 | /** 62 | * 63 | * @param id 64 | * @param {Janus} janus 65 | */ 66 | constructor(id, janus) { 67 | this.id = id; 68 | this.janus = janus; 69 | this.handleID = null; 70 | } 71 | 72 | connect = async () => { 73 | return new Promise(((resolve, reject) => { 74 | this.janus.socket.send({ 75 | 'janus': 'attach', 76 | 'plugin': this.id, 77 | 'opaque_id': `janus-${JanusUtils.randomString(12)}`, 78 | 'session_id': this.janus.socket.sessionID, 79 | }, (response) => { 80 | if (response.janus === 'success') { 81 | this.handleID = response.data.id; 82 | this.janus.socket.attachPlugin(this); 83 | resolve(); 84 | return; 85 | } 86 | reject(); 87 | }); 88 | })); 89 | }; 90 | 91 | createPeer = async () => { 92 | this.pc = new Janus.RTCPeerConnection({ 93 | 'iceServers': [ 94 | ...iceServers['iceServers'], 95 | ...this.janus.iceServers, 96 | ] 97 | }); 98 | 99 | this.pc.onicecandidate = async (event) => { 100 | if (!event.candidate || event.candidate.candidate.indexOf('endOfCandidates') > 0) { 101 | 102 | } else { 103 | await this.sendCandidateAsync(event.candidate); 104 | } 105 | }; 106 | 107 | this.pc.onaddstream = (event) => { 108 | if (event && event.stream) { 109 | if (this.onStreamListener && typeof this.onStreamListener === 'function') { 110 | this.onStreamListener(event.stream); 111 | } 112 | } 113 | }; 114 | 115 | this.pc.onremovestream = (event) => { 116 | //fixme: what to do on remove stream? 117 | console.error('onremovestream', event); 118 | if (this.onStreamListener && typeof this.onStreamListener === 'function') { 119 | this.onStreamListener(null); 120 | } 121 | }; 122 | 123 | this.pc.onnegotiationneeded = (e) => { 124 | console.log('onnegotiationneeded', e.target); 125 | }; 126 | 127 | this.pc.onconnectionstatechange = (e) => { 128 | e.target.connectionState; // new, connected, connecting 129 | e.target.signalingState; // stable, have-local-offer, have-remote-offer, have-local-pranswer, have-remote-pranswer, closed 130 | e.target.iceConnectionState; // new, checking, connected, completed, failed, disconnected, closed 131 | e.target.iceGatheringState; // new, complete, gathering 132 | console.log('connectionState', e.target.connectionState); 133 | }; 134 | 135 | this.pc.onsignalingstatechange = (e) => { 136 | console.log('signalingState', e.target.signalingState); 137 | }; 138 | 139 | this.pc.onicecandidateerror = (e) => { 140 | console.log('onicecandidateerror', e); 141 | }; 142 | 143 | this.pc.onicegatheringstatechange = (e) => { 144 | console.log('iceConnectionState', e.target.iceConnectionState); 145 | }; 146 | 147 | this.pc.oniceconnectionstatechange = (e) => { 148 | console.log('iceGatheringState', e.target.iceGatheringState); 149 | }; 150 | }; 151 | 152 | /** 153 | * 154 | * @param listener {onWebRTCUpListener} 155 | */ 156 | setOnWebRTCUpListener = (listener) => { 157 | this.onWebRTCUpListener = listener; 158 | }; 159 | 160 | /** 161 | * 162 | * @param listener {onStreamListener} 163 | */ 164 | setOnStreamListener = (listener) => { 165 | this.onStreamListener = listener; 166 | }; 167 | 168 | send = (request, callback) => { 169 | return this.janus.socket.send({ 170 | 'janus': 'message', 171 | 'session_id': this.janus.socket.sessionID, 172 | 'handle_id': this.handleID, 173 | 'body': request, 174 | }, callback); 175 | }; 176 | 177 | sendAsyncWithJsep = (request, jsep) => { 178 | return this.janus.socket.sendAsync({ 179 | 'janus': 'message', 180 | 'session_id': this.janus.socket.sessionID, 181 | 'handle_id': this.handleID, 182 | 'body': request, 183 | 'jsep': jsep, 184 | }); 185 | }; 186 | 187 | sendCandidateAsync = (candidate) => { 188 | return this.janus.socket.sendAsync({ 189 | 'janus': 'trickle', 190 | 'session_id': this.janus.socket.sessionID, 191 | 'handle_id': this.handleID, 192 | 'candidate': candidate, 193 | }); 194 | }; 195 | 196 | sendAsync = (request) => { 197 | let additionalConfig = {}; 198 | if (this.janus.apiSecret) { 199 | additionalConfig['apisecret'] = this.janus.apiSecret; 200 | } 201 | return this.janus.socket.sendAsync({ 202 | 'janus': 'message', 203 | 'session_id': this.janus.socket.sessionID, 204 | 'handle_id': this.handleID, 205 | 'body': request, 206 | ...additionalConfig, 207 | }); 208 | }; 209 | 210 | detach = () => { 211 | let additionalConfig = {}; 212 | 213 | if (this.janus.apiSecret) { 214 | additionalConfig['apisecret'] = this.janus.apiSecret; 215 | } 216 | 217 | return this.janus.socket.sendAsync({ 218 | 'janus': 'detach', 219 | 'session_id': this.janus.socket.sessionID, 220 | 'handle_id': this.handleID, 221 | ...additionalConfig, 222 | }); 223 | }; 224 | } 225 | 226 | export default JanusPlugin; 227 | -------------------------------------------------------------------------------- /plugins/streaming/JanusStreamingPlugin.js: -------------------------------------------------------------------------------- 1 | import JanusPlugin from './../../utils/JanusPlugin'; 2 | import Janus from './../../Janus'; 3 | 4 | export default class JanusStreamingPlugin extends JanusPlugin { 5 | /** 6 | * 7 | * @param {Janus} janus 8 | */ 9 | constructor(janus) { 10 | super('janus.plugin.streaming', janus); 11 | 12 | this.isRemoteDescriptionSet = false; 13 | this.cachedCandidates = []; 14 | } 15 | 16 | onMessage = async (message) => { 17 | switch (message.janus) { 18 | case 'webrtcup': { 19 | this.isWebRtcUp = true; 20 | if (this.onWebRTCUpListener && typeof this.onWebRTCUpListener === 'function') { 21 | this.onWebRTCUpListener(); 22 | } 23 | return; 24 | } 25 | 26 | case 'media': { 27 | if (message.type === 'audio') { 28 | this.isReceivingAudio = message.receiving; 29 | console.log('plugin', (message.receiving ? '' : 'not ') + 'receiving audio now...'); 30 | } else if (message.type === 'video') { 31 | this.isReceivingVideo = message.receiving; 32 | console.log('plugin', (message.receiving ? '' : 'not ') + 'receiving video now...'); 33 | } 34 | return; 35 | } 36 | 37 | case 'trickle': { 38 | 39 | if (this.isRemoteDescriptionSet) { 40 | await this.pc.addIceCandidate(new Janus.RTCIceCandidate({ 41 | candidate: message.candidate.candidate, 42 | sdpMid: message.candidate.sdpMid, 43 | sdpMLineIndex: message.candidate.sdpMLineIndex, 44 | })); 45 | } 46 | 47 | this.cachedCandidates.push(new Janus.RTCIceCandidate({ 48 | candidate: message.candidate.candidate, 49 | sdpMid: message.candidate.sdpMid, 50 | sdpMLineIndex: message.candidate.sdpMLineIndex, 51 | })); 52 | return; 53 | } 54 | 55 | case 'hangup': { 56 | message.reason; 57 | console.log('plugin', 'hangup', message.reason); 58 | return; 59 | } 60 | 61 | case 'detached': { 62 | console.log('plugin', 'detached'); 63 | return; 64 | } 65 | 66 | case 'slowlink': { 67 | console.log('plugin', 'slowlink', message); 68 | return; 69 | } 70 | 71 | case 'event': { 72 | const data = message.plugindata.data; 73 | console.log('plugin', 'event', data); 74 | return; 75 | } 76 | } 77 | }; 78 | 79 | /** 80 | * 81 | * @param id 82 | * @param pin 83 | * @returns {Promise} 84 | */ 85 | watch = async (id, {pin}) => { 86 | try { 87 | let additionalConfig = {}; 88 | 89 | if (pin) { 90 | additionalConfig.pin = additionalConfig; 91 | } 92 | 93 | const watchStreamingResponse = await this.sendAsync({ 94 | 'request': 'watch', 95 | 'id': id, 96 | 'offer_audio': true, 97 | 'offer_video': true, 98 | 'offer_data': false, 99 | ...additionalConfig, 100 | }); 101 | 102 | 103 | if (watchStreamingResponse.jsep) { 104 | await this.pc.setRemoteDescription(new Janus.RTCSessionDescription({ 105 | sdp: watchStreamingResponse.jsep.sdp, 106 | type: watchStreamingResponse.jsep.type, 107 | })); 108 | this.isRemoteDescriptionSet = true; 109 | 110 | console.error('watch', 'set remote desc'); 111 | 112 | for (const candidate of this.cachedCandidates) { 113 | await this.pc.addIceCandidate(candidate); 114 | } 115 | this.cachedCandidates = []; 116 | 117 | console.error('watch', 'added candidates'); 118 | 119 | let answer = await this.pc.createAnswer({ 120 | 'offerToReceiveAudio': true, 121 | 'offerToReceiveVideo': true, 122 | mandatory: { 123 | OfferToReceiveAudio: true, 124 | OfferToReceiveVideo: true, 125 | }, 126 | }); 127 | 128 | await this.pc.setLocalDescription(answer); 129 | 130 | const startResponse = await this.sendAsyncWithJsep({ 131 | 'request': 'start', 132 | }, { 133 | type: answer.type, 134 | sdp: answer.sdp, 135 | }); 136 | 137 | } 138 | 139 | // if (watchStreamingResponse && watchStreamingResponse.plugindata && watchStreamingResponse.plugindata.data && watchStreamingResponse.plugindata.data.streaming === 'created') { 140 | // return true; 141 | // } 142 | return false; 143 | } catch (e) { 144 | console.error('createStreamingResponse', e); 145 | } 146 | }; 147 | 148 | 149 | create = async (id, {name, description, pin, secret, audioPort, audioPt, videoPort, videoPt}) => { 150 | try { 151 | let additionalConfig = {}; 152 | 153 | if (name) { 154 | additionalConfig.name = name; 155 | } 156 | 157 | if (description) { 158 | additionalConfig.description = description; 159 | } 160 | 161 | if (pin) { 162 | additionalConfig.pin = description; 163 | } 164 | 165 | if (secret) { 166 | additionalConfig.secret = description; 167 | } 168 | 169 | const createStreamingResponse = await this.sendAsync({ 170 | 'request': 'create', 171 | 'type': 'rtp', 172 | 'id': id, 173 | 'metadata': '', 174 | 'is_private': true, 175 | 'audio': true, 176 | 'video': true, 177 | 'data': false, 178 | 'permanent': false, 179 | 180 | audioport: audioPort, 181 | audiopt: audioPt, 182 | audiortpmap: 'opus/48000/2', 183 | audiofmtp: `${audioPt}`, 184 | 185 | videoport: videoPort, 186 | videopt: videoPt, 187 | videortpmap: 'VP8/90000', 188 | videofmtp: `${videoPt}`, 189 | videobufferkf: true, 190 | 191 | ...additionalConfig, 192 | }); 193 | 194 | 195 | if (createStreamingResponse && createStreamingResponse.plugindata && createStreamingResponse.plugindata.data && createStreamingResponse.plugindata.data.streaming === 'created') { 196 | return true; 197 | } 198 | 199 | console.error('createStreamingResponse', createStreamingResponse); 200 | return false; 201 | } catch (e) { 202 | console.error('createStreamingResponse', e); 203 | } 204 | }; 205 | 206 | destroy = async (id) => { 207 | try { 208 | const destroyStreamingResponse = await this.sendAsync({ 209 | 'request': 'destroy', 210 | 'id': id, 211 | 'secret': '1234', 212 | 'permanent': false, 213 | }); 214 | 215 | if (destroyStreamingResponse && destroyStreamingResponse.plugindata && destroyStreamingResponse.plugindata.data && destroyStreamingResponse.plugindata.data.destroyed === id) { 216 | return true; 217 | } 218 | 219 | console.error('destroyStreamingResponse', destroyStreamingResponse); 220 | return false; 221 | } catch (e) { 222 | console.error('createStreamingResponse', e); 223 | } 224 | }; 225 | } 226 | -------------------------------------------------------------------------------- /plugins/audiobridge/JanusAudioBridgePlugin.js: -------------------------------------------------------------------------------- 1 | import JanusPlugin from "calldemo/node_modules/react-native-janus/utils/JanusPlugin.js"; 2 | import { Janus } from "../../node_modules/react-native-janus"; 3 | import JanusAudioBridgeParticipant from "./AudioBridgeParticipant"; 4 | 5 | export default class JanusAudioBridgePulgin extends JanusPlugin { 6 | /** 7 | * Array of audio bridge participants 8 | * @type {JanusAudioBridgeParticipant[]} 9 | */ 10 | participants = null; 11 | 12 | /** 13 | * 14 | * @callback onParticipantsListener 15 | * @param {JanusAudioBridgeParticipant[]} participants 16 | */ 17 | /** 18 | * 19 | * @type {onPublishersListener} 20 | */ 21 | onPublishersListener = null; 22 | /** 23 | * 24 | * @callback onParticipantLeftListener 25 | * @param {number} participantID 26 | */ 27 | /** 28 | * 29 | * @type {onParticipantLeftListener} 30 | */ 31 | onParticipantLeftListener = null; 32 | /** 33 | * 34 | * @param {Janus} janus 35 | */ 36 | /** 37 | * 38 | * @callback onParticipantJoinedListener 39 | * @param {onParticipantJoinedListener} participant 40 | */ 41 | /** 42 | * 43 | * @type {onParticipantJoinedListener} 44 | */ 45 | onParticipantJoinedListener = null; 46 | constructor(janus) { 47 | super("janus.plugin.audiobridge", janus); 48 | 49 | this.userID = null; 50 | this.participants = null; 51 | 52 | this.roomID = null; 53 | this.displayName = null; 54 | this.stream = null; 55 | 56 | this.isRemoteDescriptionSet = false; 57 | } 58 | 59 | getUserID = () => this.userID; 60 | 61 | /** 62 | * 63 | * @param {Number} roomID 64 | */ 65 | setRoomID = (roomID) => { 66 | this.roomID = roomID; 67 | }; 68 | 69 | /** 70 | * 71 | * @param {String} displayName 72 | */ 73 | setDisplayName = (displayName) => { 74 | this.displayName = displayName; 75 | }; 76 | 77 | /** 78 | * 79 | * @param listener {onParticipantsListener} 80 | */ 81 | setOnParticipantsListener = (listener) => { 82 | this.onParticipantsListener = listener; 83 | }; 84 | /** 85 | * 86 | * @param listener {onParticipantJoinedListener} 87 | */ 88 | setOnParticipantJoinedListener = (listener) => { 89 | this.onParticipantJoinedListener = listener; 90 | }; 91 | 92 | /** 93 | * 94 | * @param listener {onParticipantLeftListener} 95 | */ 96 | setOnParticipantLeftListener = (listener) => { 97 | this.onParticipantLeftListener = listener; 98 | }; 99 | 100 | onMessage = async (message) => { 101 | switch (message.janus) { 102 | case "webrtcup": { 103 | this.isWebRtcUp = true; 104 | if ( 105 | this.onWebRTCUpListener && 106 | typeof this.onWebRTCUpListener === "function" 107 | ) { 108 | this.onWebRTCUpListener(); 109 | } 110 | return; 111 | } 112 | 113 | case "media": { 114 | if (message.type === "audio") { 115 | this.isReceivingAudio = message.receiving; 116 | console.log( 117 | "plugin", 118 | (message.receiving ? "" : "not ") + "receiving audio now..." 119 | ); 120 | } else if (message.type === "video") { 121 | this.isReceivingVideo = message.receiving; 122 | console.log( 123 | "plugin", 124 | (message.receiving ? "" : "not ") + "receiving video now..." 125 | ); 126 | } 127 | return; 128 | } 129 | 130 | case "trickle": { 131 | console.log("got trickle"); 132 | if (this.isRemoteDescriptionSet) { 133 | console.log("adding ice to pc"); 134 | await this.pc.addIceCandidate( 135 | new Janus.RTCIceCandidate({ 136 | candidate: message.candidate.candidate, 137 | sdpMid: message.candidate.sdpMid, 138 | sdpMLineIndex: message.candidate.sdpMLineIndex, 139 | }) 140 | ); 141 | } 142 | 143 | this.cachedCandidates.push( 144 | new Janus.RTCIceCandidate({ 145 | candidate: message.candidate.candidate, 146 | sdpMid: message.candidate.sdpMid, 147 | sdpMLineIndex: message.candidate.sdpMLineIndex, 148 | }) 149 | ); 150 | 151 | return; 152 | } 153 | 154 | case "hangup": { 155 | message.reason; 156 | console.log("plugin", "hangup", message.reason); 157 | return; 158 | } 159 | 160 | case "detached": { 161 | console.log("plugin", "detached"); 162 | return; 163 | } 164 | 165 | case "slowlink": { 166 | console.log("plugin", "slowlink", message); 167 | return; 168 | } 169 | 170 | case "event": { 171 | const data = message.plugindata.data; 172 | 173 | if (data.audiobridge === "event") { 174 | if (data.room === this.roomID) { 175 | if (typeof data["leaving"] !== "undefined") { 176 | let leavingParticipantID = data["leaving"]; 177 | if ( 178 | this.onParticipantLeftListener != null && 179 | typeof this.onParticipantLeftListener === "function" 180 | ) { 181 | this.onParticipantLeftListener(leavingParticipantID); 182 | } 183 | return; 184 | } else if (typeof data["publishers"] !== "undefined") { 185 | let newParticipants = data["participants"].map( 186 | (participantsrData) => 187 | new JanusAudioBridgeParticipant(participantsrData) 188 | ); 189 | for (let i = 0; i < newParticipants.length; i++) { 190 | if ( 191 | this.onParticipantJoinedListener != null && 192 | typeof this.onParticipantJoinedListener === "function" 193 | ) { 194 | this.onParticipantJoinedListener(newParticipants[i]); 195 | } 196 | } 197 | return; 198 | } 199 | } 200 | } 201 | 202 | console.log("plugin", "event", data); 203 | return; 204 | } 205 | } 206 | }; 207 | 208 | /** 209 | * 210 | * @returns {Promise} 211 | */ 212 | join = async (roomid) => { 213 | try { 214 | let joinResponse = await this.sendAsync({ 215 | request: "join", 216 | room: roomid, 217 | display: this.displayName, 218 | muted: false, 219 | }); 220 | 221 | if ( 222 | joinResponse.janus === "event" && 223 | joinResponse.plugindata && 224 | joinResponse.plugindata.data 225 | ) { 226 | let data = joinResponse.plugindata.data; 227 | if (data.audiobridge === "joined") { 228 | this.userID = data.id; 229 | this.participants = data.participants.map( 230 | (participantsrData) => 231 | new JanusAudioBridgeParticipant(participantsrData) 232 | ); 233 | 234 | if ( 235 | this.onParticipantsListener != null && 236 | typeof this.onParticipantsListener === "function" 237 | ) { 238 | this.onParticipantsListener(this.participants); 239 | } 240 | 241 | return; 242 | } else { 243 | console.error("join", joinResponse); 244 | } 245 | } else { 246 | console.error("join", joinResponse); 247 | } 248 | } catch (e) { 249 | console.error("join", e); 250 | } 251 | }; 252 | 253 | /** 254 | * 255 | * @param {Janus.MediaStream} stream 256 | * @returns {Promise} 257 | */ 258 | configure = async (stream) => { 259 | try { 260 | console.log("joined to room."); 261 | 262 | this.pc.addStream(stream); 263 | 264 | let offer = await this.pc.createOffer({ 265 | offerToReceiveAudio: true, 266 | offerToReceiveVideo: false, 267 | }); 268 | 269 | await this.pc.setLocalDescription(offer); 270 | 271 | // offer.sdp = offer.sdp.replace(/a=extmap:(\d+) urn:3gpp:video-orientation\n?/, ''); 272 | 273 | let response = await this.sendAsyncWithJsep( 274 | { 275 | request: "configure", 276 | muted: false, 277 | // recording preferences 278 | record: true, 279 | filename: "recording.wav", 280 | }, 281 | { 282 | type: offer.type, 283 | sdp: offer.sdp, 284 | } 285 | ); 286 | 287 | await this.pc.setRemoteDescription( 288 | new Janus.RTCSessionDescription({ 289 | sdp: response.jsep.sdp, 290 | type: response.jsep.type, 291 | }) 292 | ); 293 | this.isRemoteDescriptionSet = true; 294 | 295 | for (const candidate of this.cachedCandidates) { 296 | await this.pc.addIceCandidate(candidate); 297 | } 298 | this.cachedCandidates = []; 299 | } catch (e) { 300 | console.log(e); 301 | } 302 | }; 303 | 304 | detach = async () => { 305 | try { 306 | let additionalConfig = {}; 307 | 308 | if (this.janus.apiSecret) { 309 | additionalConfig["apisecret"] = this.janus.apiSecret; 310 | } 311 | 312 | const hangupResponse = await this.janus.socket.sendAsync({ 313 | janus: "hangup", 314 | session_id: this.janus.socket.sessionID, 315 | handle_id: this.handleID, 316 | ...additionalConfig, 317 | }); 318 | 319 | if (hangupResponse.janus === "success") { 320 | } 321 | 322 | const detachResponse = await this.detach(); 323 | 324 | if (detachResponse.janus === "success") { 325 | } 326 | 327 | this.pc.close(); 328 | this.janus.socket.detachPlugin(this); 329 | 330 | console.error("detach", "hangupResponse", hangupResponse); 331 | console.error("detach", "detachResponse", detachResponse); 332 | } catch (e) { 333 | console.error("detach", e); 334 | } 335 | }; 336 | 337 | create = async (room) => { 338 | try { 339 | let createResponse = await this.sendAsync({ 340 | request: "create", 341 | room: room, 342 | description: "demo", 343 | record: true, 344 | }); 345 | this.setRoomID(room); 346 | 347 | // todo finish response handling 348 | console.log("create respone", createResponse); 349 | } catch (e) { 350 | console.log("create", e); 351 | } 352 | }; 353 | } 354 | -------------------------------------------------------------------------------- /plugins/videoroom/JanusVideoRoomPlugin.js: -------------------------------------------------------------------------------- 1 | import JanusVideoRoomPublisher from "./JanusVideoRoomPublisher"; 2 | import JanusPlugin from "./../../utils/JanusPlugin"; 3 | import Janus from "./../../Janus"; 4 | 5 | export default class JanusVideoRoomPlugin extends JanusPlugin { 6 | /** 7 | * Array of video room publishers 8 | * @type {JanusVideoRoomPublisher[]} 9 | */ 10 | publishers = null; 11 | 12 | /** 13 | * 14 | * @type {Janus.MediaStream} 15 | */ 16 | stream = null; 17 | 18 | /** 19 | * 20 | * @callback onPublishersListener 21 | * @param {JanusVideoRoomPublisher[]} publishers 22 | */ 23 | /** 24 | * 25 | * @type {onPublishersListener} 26 | */ 27 | onPublishersListener = null; 28 | /** 29 | * 30 | * @callback onPublisherJoinedListener 31 | * @param {JanusVideoRoomPublisher} publisher 32 | */ 33 | /** 34 | * 35 | * @type {onPublisherJoinedListener} 36 | */ 37 | onPublisherJoinedListener = null; 38 | /** 39 | * 40 | * @callback onPublisherLeftListener 41 | * @param {number} publisherID 42 | */ 43 | /** 44 | * 45 | * @type {onPublisherLeftListener} 46 | */ 47 | onPublisherLeftListener = null; 48 | /** 49 | * 50 | * @callback onPublisherUpdatedListener 51 | * @param {JanusVideoRoomPublisher} publisher 52 | */ 53 | /** 54 | * 55 | * @type {onPublisherUpdatedListener} 56 | */ 57 | onPublisherUpdatedListener = null; 58 | /** 59 | * 60 | * @callback onStreamAddedListener 61 | * @param {Janus.MediaStream} stream 62 | */ 63 | /** 64 | * 65 | * @type {onStreamAddedListener} 66 | */ 67 | onStreamAddedListener = null; 68 | /** 69 | * 70 | * @callback onStreamRemovedListener 71 | * @param {Janus.MediaStream} stream 72 | */ 73 | /** 74 | * 75 | * @type {onStreamRemovedListener} 76 | */ 77 | onStreamRemovedListener = null; 78 | 79 | /** 80 | * 81 | * @param {Janus} janus 82 | */ 83 | constructor(janus) { 84 | super("janus.plugin.videoroom", janus); 85 | 86 | this.userID = null; 87 | this.userPrivateID = null; 88 | this.publishers = null; 89 | 90 | this.roomID = null; 91 | this.displayName = null; 92 | this.stream = null; 93 | 94 | this.isRemoteDescriptionSet = false; 95 | } 96 | 97 | getUserID = () => this.userID; 98 | getUserPrivateID = () => this.userPrivateID; 99 | 100 | /** 101 | * 102 | * @param {Number} roomID 103 | */ 104 | setRoomID = (roomID) => { 105 | this.roomID = roomID; 106 | }; 107 | 108 | /** 109 | * 110 | * @param {String} displayName 111 | */ 112 | setDisplayName = (displayName) => { 113 | this.displayName = displayName; 114 | }; 115 | 116 | /** 117 | * 118 | * @returns {JanusVideoRoomPublisher[]} 119 | */ 120 | getPublishers = () => { 121 | return this.publishers; 122 | }; 123 | 124 | /** 125 | * 126 | * @param listener {onPublishersListener} 127 | */ 128 | setOnPublishersListener = (listener) => { 129 | this.onPublishersListener = listener; 130 | }; 131 | 132 | /** 133 | * 134 | * @param listener {onPublisherJoinedListener} 135 | */ 136 | setOnPublisherJoinedListener = (listener) => { 137 | this.onPublisherJoinedListener = listener; 138 | }; 139 | 140 | /** 141 | * 142 | * @param listener {onPublisherLeftListener} 143 | */ 144 | setOnPublisherLeftListener = (listener) => { 145 | this.onPublisherLeftListener = listener; 146 | }; 147 | 148 | /** 149 | * 150 | * @param listener {setOnPublisherUpdatedListener} 151 | */ 152 | setOnPublisherUpdatedListener = (listener) => { 153 | this.onPublisherUpdatedListener = listener; 154 | }; 155 | 156 | /** 157 | * 158 | * @param listener {onStreamAddedListener} 159 | */ 160 | setOnStreamAddedListener = (listener) => { 161 | this.onStreamAddedListener = listener; 162 | }; 163 | 164 | /** 165 | * 166 | * @param listener {onStreamRemovedListener} 167 | */ 168 | setOnStreamRemovedListener = (listener) => { 169 | this.onStreamRemovedListener = listener; 170 | }; 171 | 172 | onMessage = async (message) => { 173 | switch (message.janus) { 174 | case "webrtcup": { 175 | this.isWebRtcUp = true; 176 | if ( 177 | this.onWebRTCUpListener && 178 | typeof this.onWebRTCUpListener === "function" 179 | ) { 180 | this.onWebRTCUpListener(); 181 | } 182 | return; 183 | } 184 | 185 | case "media": { 186 | if (message.type === "audio") { 187 | this.isReceivingAudio = message.receiving; 188 | console.log( 189 | "plugin", 190 | (message.receiving ? "" : "not ") + "receiving audio now..." 191 | ); 192 | } else if (message.type === "video") { 193 | this.isReceivingVideo = message.receiving; 194 | console.log( 195 | "plugin", 196 | (message.receiving ? "" : "not ") + "receiving video now..." 197 | ); 198 | } 199 | return; 200 | } 201 | 202 | case "trickle": { 203 | if (this.isRemoteDescriptionSet) { 204 | await this.pc.addIceCandidate( 205 | new Janus.RTCIceCandidate({ 206 | candidate: message.candidate.candidate, 207 | sdpMid: message.candidate.sdpMid, 208 | sdpMLineIndex: message.candidate.sdpMLineIndex, 209 | }) 210 | ); 211 | } 212 | 213 | this.cachedCandidates.push( 214 | new Janus.RTCIceCandidate({ 215 | candidate: message.candidate.candidate, 216 | sdpMid: message.candidate.sdpMid, 217 | sdpMLineIndex: message.candidate.sdpMLineIndex, 218 | }) 219 | ); 220 | 221 | return; 222 | } 223 | 224 | case "hangup": { 225 | message.reason; 226 | console.log("plugin", "hangup", message.reason); 227 | return; 228 | } 229 | 230 | case "detached": { 231 | console.log("plugin", "detached"); 232 | return; 233 | } 234 | 235 | case "slowlink": { 236 | console.log("plugin", "slowlink", message); 237 | return; 238 | } 239 | 240 | case "event": { 241 | const data = message.plugindata.data; 242 | 243 | if (data.videoroom === "event") { 244 | if (data.room === this.roomID) { 245 | if (typeof data["unpublished"] !== "undefined") { 246 | // let unpublishedPublisherID = data['unpublished']; 247 | // let leavingPublisherID = data['leaving']; 248 | // if (this.onPublisherLeftListener != null && typeof this.onPublisherLeftListener === 'function') { 249 | // this.onPublisherLeftListener(leavingPublisherID); 250 | // } 251 | return; 252 | } else if (typeof data["leaving"] !== "undefined") { 253 | let leavingPublisherID = data["leaving"]; 254 | if ( 255 | this.onPublisherLeftListener != null && 256 | typeof this.onPublisherLeftListener === "function" 257 | ) { 258 | this.onPublisherLeftListener(leavingPublisherID); 259 | } 260 | return; 261 | } else if (typeof data["publishers"] !== "undefined") { 262 | let newPublishers = data["publishers"].map( 263 | (publisherData) => new JanusVideoRoomPublisher(publisherData) 264 | ); 265 | for (let i = 0; i < newPublishers.length; i++) { 266 | if ( 267 | this.onPublisherJoinedListener != null && 268 | typeof this.onPublisherJoinedListener === "function" 269 | ) { 270 | this.onPublisherJoinedListener(newPublishers[i]); 271 | } 272 | } 273 | return; 274 | } 275 | } 276 | } 277 | 278 | console.log("plugin", "event", data); 279 | return; 280 | } 281 | } 282 | }; 283 | 284 | forward = async ({ 285 | host, 286 | audioPort, 287 | audioPt, 288 | videoPort, 289 | videoPt, 290 | secret, 291 | }) => { 292 | try { 293 | let additionalConfig = {}; 294 | 295 | if (secret) { 296 | additionalConfig.secret = secret; 297 | } 298 | 299 | const rtpForwardResponse = await this.sendAsync({ 300 | request: "rtp_forward", 301 | publisher_id: this.userID, 302 | room: this.roomID, 303 | audio_port: audioPort, 304 | audio_pt: audioPt, 305 | video_port: videoPort, 306 | video_pt: videoPt, 307 | host: host, 308 | ...additionalConfig, 309 | }); 310 | 311 | if ( 312 | rtpForwardResponse && 313 | rtpForwardResponse.plugindata && 314 | rtpForwardResponse.plugindata.data && 315 | rtpForwardResponse.plugindata.data.rtp_stream 316 | ) { 317 | return true; 318 | } 319 | 320 | console.error("rtpForwardResponse", "success!", rtpForwardResponse); 321 | } catch (e) { 322 | console.error("streaming", e); 323 | } 324 | }; 325 | 326 | muteAudio() {} 327 | 328 | unmuteAudio() {} 329 | 330 | closeVideo() {} 331 | 332 | openVideo() {} 333 | 334 | /** 335 | * 336 | * @returns {Promise} 337 | */ 338 | join = async () => { 339 | try { 340 | let joinResponse = await this.sendAsync({ 341 | request: "join", 342 | room: this.roomID, 343 | display: this.displayName, 344 | ptype: "publisher", 345 | }); 346 | 347 | if ( 348 | joinResponse.janus === "event" && 349 | joinResponse.plugindata && 350 | joinResponse.plugindata.data 351 | ) { 352 | let data = joinResponse.plugindata.data; 353 | if (data.videoroom === "joined") { 354 | this.userID = data.id; 355 | this.userPrivateID = data.private_id; 356 | this.publishers = data.publishers.map( 357 | (publisherData) => new JanusVideoRoomPublisher(publisherData) 358 | ); 359 | 360 | if ( 361 | this.onPublishersListener != null && 362 | typeof this.onPublishersListener === "function" 363 | ) { 364 | this.onPublishersListener(this.publishers); 365 | } 366 | 367 | return; 368 | } else { 369 | console.error("join", joinResponse); 370 | } 371 | } else { 372 | console.error("join", joinResponse); 373 | } 374 | } catch (e) { 375 | console.error("join", e); 376 | } 377 | }; 378 | 379 | /** 380 | * 381 | * @returns {Promise} 382 | */ 383 | leave = async () => {}; 384 | 385 | /** 386 | * 387 | * @param {Janus.MediaStream} stream 388 | * @returns {Promise} 389 | */ 390 | publish = async (stream, audio = true, video = true) => { 391 | try { 392 | console.log("joined to room."); 393 | 394 | this.pc.addStream(stream); 395 | 396 | let offer = await this.pc.createOffer({ 397 | offerToReceiveAudio: false, 398 | offerToReceiveVideo: false, 399 | }); 400 | 401 | await this.pc.setLocalDescription(offer); 402 | 403 | // offer.sdp = offer.sdp.replace(/a=extmap:(\d+) urn:3gpp:video-orientation\n?/, ''); 404 | 405 | let response = await this.sendAsyncWithJsep( 406 | { 407 | request: "configure", 408 | audio: audio, 409 | video: video, 410 | bitrate: 128 * 1000, 411 | }, 412 | { 413 | type: offer.type, 414 | sdp: offer.sdp, 415 | } 416 | ); 417 | 418 | await this.pc.setRemoteDescription( 419 | new Janus.RTCSessionDescription({ 420 | sdp: response.jsep.sdp, 421 | type: response.jsep.type, 422 | }) 423 | ); 424 | this.isRemoteDescriptionSet = true; 425 | 426 | for (const candidate of this.cachedCandidates) { 427 | await this.pc.addIceCandidate(candidate); 428 | } 429 | this.cachedCandidates = []; 430 | } catch (e) { 431 | console.log(e); 432 | } 433 | }; 434 | 435 | /** 436 | * @returns {Promise} 437 | */ 438 | unpublish = async () => { 439 | try { 440 | const unpublishResponse = await this.sendAsync({ 441 | request: "unpublish", 442 | }); 443 | 444 | if ( 445 | unpublishResponse && 446 | unpublishResponse.plugindata && 447 | unpublishResponse.plugindata.data && 448 | unpublishResponse.plugindata.data.unpublished === "ok" 449 | ) { 450 | return; 451 | } 452 | 453 | console.error("unpublishResponse", unpublishResponse); 454 | } catch (e) { 455 | console.error("unpublish", e); 456 | } 457 | }; 458 | 459 | /** 460 | * 461 | * @param privateID 462 | * @param {JanusVideoRoomPublisher} publisher 463 | * @returns {Promise} 464 | */ 465 | receive = async (privateID, publisher, audio = true, video = true) => { 466 | try { 467 | let joinResponse = await this.sendAsync({ 468 | request: "join", 469 | room: this.roomID, 470 | ptype: "subscriber", 471 | feed: publisher.id, 472 | private_id: privateID, 473 | }); 474 | 475 | if ( 476 | joinResponse && 477 | joinResponse.plugindata && 478 | joinResponse.plugindata.data && 479 | joinResponse.plugindata.data.videoroom === "attached" 480 | ) { 481 | // OK 482 | } 483 | 484 | await this.pc.setRemoteDescription( 485 | new Janus.RTCSessionDescription({ 486 | sdp: joinResponse.jsep.sdp, 487 | type: joinResponse.jsep.type, 488 | }) 489 | ); 490 | this.isRemoteDescriptionSet = true; 491 | 492 | for (const candidate of this.cachedCandidates) { 493 | await this.pc.addIceCandidate(candidate); 494 | } 495 | this.cachedCandidates = []; 496 | 497 | let answer = await this.pc.createAnswer({ 498 | offerToReceiveAudio: audio, 499 | offerToReceiveVideo: video, 500 | }); 501 | 502 | await this.pc.setLocalDescription(answer); 503 | 504 | const startResponse = await this.sendAsyncWithJsep( 505 | { 506 | request: "start", 507 | room: this.roomID, 508 | }, 509 | { 510 | type: answer.type, 511 | sdp: answer.sdp, 512 | } 513 | ); 514 | 515 | if ( 516 | startResponse && 517 | startResponse.plugindata && 518 | startResponse.plugindata.data && 519 | startResponse.plugindata.data.started === "ok" 520 | ) { 521 | // OK 522 | } 523 | } catch (e) { 524 | console.error("receive", e); 525 | } 526 | }; 527 | 528 | detach = async () => { 529 | try { 530 | let additionalConfig = {}; 531 | 532 | if (this.janus.apiSecret) { 533 | additionalConfig["apisecret"] = this.janus.apiSecret; 534 | } 535 | 536 | const hangupResponse = await this.janus.socket.sendAsync({ 537 | janus: "hangup", 538 | session_id: this.janus.socket.sessionID, 539 | handle_id: this.handleID, 540 | ...additionalConfig, 541 | }); 542 | 543 | if (hangupResponse.janus === "success") { 544 | } 545 | 546 | const detachResponse = await this.detach(); 547 | 548 | if (detachResponse.janus === "success") { 549 | } 550 | 551 | this.pc.close(); 552 | this.janus.socket.detachPlugin(this); 553 | 554 | console.error("detach", "hangupResponse", hangupResponse); 555 | console.error("detach", "detachResponse", detachResponse); 556 | } catch (e) { 557 | console.error("detach", e); 558 | } 559 | }; 560 | 561 | create = ({}) => {}; 562 | 563 | exists = () => {}; 564 | 565 | destroy = () => {}; 566 | 567 | listAll = () => {}; 568 | 569 | listParticipants = () => {}; 570 | 571 | update = () => {}; 572 | } 573 | --------------------------------------------------------------------------------