UNPKG

react-native-webrtc

Version:
831 lines (647 loc) 29.2 kB
import { EventTarget, Event, defineEventAttribute } from 'event-target-shim/index'; import { NativeModules } from 'react-native'; import { addListener, removeListener } from './EventEmitter'; import Logger from './Logger'; import MediaStream from './MediaStream'; import MediaStreamTrack from './MediaStreamTrack'; import MediaStreamTrackEvent from './MediaStreamTrackEvent'; import RTCDataChannel from './RTCDataChannel'; import RTCDataChannelEvent from './RTCDataChannelEvent'; import RTCIceCandidate from './RTCIceCandidate'; import RTCIceCandidateEvent from './RTCIceCandidateEvent'; import RTCRtpReceiveParameters from './RTCRtpReceiveParameters'; import RTCRtpReceiver from './RTCRtpReceiver'; import RTCRtpSendParameters from './RTCRtpSendParameters'; import RTCRtpSender from './RTCRtpSender'; import RTCRtpTransceiver from './RTCRtpTransceiver'; import RTCSessionDescription, { RTCSessionDescriptionInit } from './RTCSessionDescription'; import RTCTrackEvent from './RTCTrackEvent'; import * as RTCUtil from './RTCUtil'; const log = new Logger('pc'); const { WebRTCModule } = NativeModules; type RTCSignalingState = | 'stable' | 'have-local-offer' | 'have-remote-offer' | 'have-local-pranswer' | 'have-remote-pranswer' | 'closed'; type RTCIceGatheringState = 'new' | 'gathering' | 'complete'; type RTCPeerConnectionState = 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'; type RTCIceConnectionState = 'new' | 'checking' | 'connected' | 'completed' | 'failed' | 'disconnected' | 'closed'; type RTCDataChannelInit = { ordered?: boolean, maxPacketLifeTime?: number, maxRetransmits?: number, protocol?: string, negotiated?: boolean, id?: number }; type RTCIceServer = { credential?: string, url?: string, // Deprecated. urls?: string | string[], username?: string }; type RTCConfiguration = { bundlePolicy?: 'balanced' | 'max-compat' | 'max-bundle', iceCandidatePoolSize?: number, iceServers?: RTCIceServer[], iceTransportPolicy?: 'all' | 'relay', rtcpMuxPolicy?: 'negotiate' | 'require' }; type RTCPeerConnectionEventMap = { connectionstatechange: Event<'connectionstatechange'> icecandidate: RTCIceCandidateEvent<'icecandidate'> icecandidateerror: RTCIceCandidateEvent<'icecandidateerror'> iceconnectionstatechange: Event<'iceconnectionstatechange'> icegatheringstatechange: Event<'icegatheringstatechange'> negotiationneeded: Event<'negotiationneeded'> signalingstatechange: Event<'signalingstatechange'> datachannel: RTCDataChannelEvent<'datachannel'> track: RTCTrackEvent<'track'> error: Event<'error'> } let nextPeerConnectionId = 0; export default class RTCPeerConnection extends EventTarget<RTCPeerConnectionEventMap> { localDescription: RTCSessionDescription | null = null; remoteDescription: RTCSessionDescription | null = null; signalingState: RTCSignalingState = 'stable'; iceGatheringState: RTCIceGatheringState = 'new'; connectionState: RTCPeerConnectionState = 'new'; iceConnectionState: RTCIceConnectionState = 'new'; _pcId: number; _transceivers: { order: number, transceiver: RTCRtpTransceiver }[]; _remoteStreams: Map<string, MediaStream>; _pendingTrackEvents: any[]; constructor(configuration?: RTCConfiguration) { super(); this._pcId = nextPeerConnectionId++; // Sanitize ICE servers. if (configuration) { const servers = configuration?.iceServers ?? []; for (const server of servers) { let urls = server.url || server.urls; delete server.url; delete server.urls; if (!urls) { continue; } if (!Array.isArray(urls)) { urls = [ urls ]; } // Native WebRTC does case sensitive parsing. server.urls = urls.map(url => url.toLowerCase()); } // Filter out bogus servers. configuration.iceServers = servers.filter(s => s.urls); } if (!WebRTCModule.peerConnectionInit(configuration, this._pcId)) { throw new Error('Failed to initialize PeerConnection, check the native logs!'); } this._transceivers = []; this._remoteStreams = new Map(); this._pendingTrackEvents = []; this._registerEvents(); log.debug(`${this._pcId} ctor`); } async createOffer(options) { log.debug(`${this._pcId} createOffer`); const { sdpInfo, newTransceivers, transceiversInfo } = await WebRTCModule.peerConnectionCreateOffer(this._pcId, RTCUtil.normalizeOfferOptions(options)); log.debug(`${this._pcId} createOffer OK`); newTransceivers?.forEach(t => { const { transceiverOrder, transceiver } = t; const newSender = new RTCRtpSender({ ...transceiver.sender, track: null }); const remoteTrack = transceiver.receiver.track ? new MediaStreamTrack(transceiver.receiver.track) : null; const newReceiver = new RTCRtpReceiver({ ...transceiver.receiver, track: remoteTrack }); const newTransceiver = new RTCRtpTransceiver({ ...transceiver, sender: newSender, receiver: newReceiver, }); this._insertTransceiverSorted(transceiverOrder, newTransceiver); }); this._updateTransceivers(transceiversInfo); return sdpInfo; } async createAnswer() { log.debug(`${this._pcId} createAnswer`); const { sdpInfo, transceiversInfo } = await WebRTCModule.peerConnectionCreateAnswer(this._pcId, {}); this._updateTransceivers(transceiversInfo); return sdpInfo; } setConfiguration(configuration): void { WebRTCModule.peerConnectionSetConfiguration(configuration, this._pcId); } async setLocalDescription(sessionDescription?: RTCSessionDescription | RTCSessionDescriptionInit): Promise<void> { log.debug(`${this._pcId} setLocalDescription`); let desc; if (sessionDescription) { desc = { type: sessionDescription.type, sdp: sessionDescription.sdp ?? '' }; if (!RTCUtil.isSdpTypeValid(desc.type)) { throw new Error(`Invalid session description: invalid type: ${desc.type}`); } } else { desc = null; } const { sdpInfo, transceiversInfo } = await WebRTCModule.peerConnectionSetLocalDescription(this._pcId, desc); if (sdpInfo.type && sdpInfo.sdp) { this.localDescription = new RTCSessionDescription(sdpInfo); } else { this.localDescription = null; } this._updateTransceivers(transceiversInfo, /* removeStopped */ desc?.type === 'answer'); log.debug(`${this._pcId} setLocalDescription OK`); } async setRemoteDescription(sessionDescription: RTCSessionDescription | RTCSessionDescriptionInit): Promise<void> { log.debug(`${this._pcId} setRemoteDescription`); if (!sessionDescription) { return Promise.reject(new Error('No session description provided')); } const desc = { type: sessionDescription.type, sdp: sessionDescription.sdp ?? '' }; if (!RTCUtil.isSdpTypeValid(desc.type ?? '')) { throw new Error(`Invalid session description: invalid type: ${desc.type}`); } const { sdpInfo, newTransceivers, transceiversInfo } = await WebRTCModule.peerConnectionSetRemoteDescription(this._pcId, desc); if (sdpInfo.type && sdpInfo.sdp) { this.remoteDescription = new RTCSessionDescription(sdpInfo); } else { this.remoteDescription = null; } newTransceivers?.forEach(t => { const { transceiverOrder, transceiver } = t; const newSender = new RTCRtpSender({ ...transceiver.sender, track: null }); const remoteTrack = transceiver.receiver.track ? new MediaStreamTrack(transceiver.receiver.track) : null; const newReceiver = new RTCRtpReceiver({ ...transceiver.receiver, track: remoteTrack }); const newTransceiver = new RTCRtpTransceiver({ ...transceiver, sender: newSender, receiver: newReceiver, }); this._insertTransceiverSorted(transceiverOrder, newTransceiver); }); this._updateTransceivers(transceiversInfo, /* removeStopped */ desc.type === 'answer'); // Fire track events. They must fire before sRD resolves. const pendingTrackEvents = this._pendingTrackEvents; this._pendingTrackEvents = []; for (const ev of pendingTrackEvents) { const [ transceiver ] = this .getTransceivers() .filter(t => t.receiver.id === ev.receiver.id); // We need to fire this event for an existing track sometimes, like // when the transceiver direction (on the sending side) switches from // sendrecv to recvonly and then back. // @ts-ignore const track: MediaStreamTrack = transceiver.receiver.track; transceiver._mid = ev.transceiver.mid; transceiver._currentDirection = ev.transceiver.currentDirection; transceiver._direction = ev.transceiver.direction; // Get the stream object from the event. Create if necessary. const streams: MediaStream[] = ev.streams.map(streamInfo => { // Here we are making sure that we don't create stream objects that already exist // So that event listeners do get the same object if it has been created before. if (!this._remoteStreams.has(streamInfo.streamId)) { const stream = new MediaStream({ streamId: streamInfo.streamId, streamReactTag: streamInfo.streamReactTag, tracks: [] }); this._remoteStreams.set(streamInfo.streamId, stream); } const stream = this._remoteStreams.get(streamInfo.streamId); if (!stream?._tracks.includes(track)) { stream?._tracks.push(track); } return stream; }); const eventData = { streams, transceiver, track, receiver: transceiver.receiver }; this.dispatchEvent(new RTCTrackEvent('track', eventData)); streams.forEach(stream => { stream.dispatchEvent(new MediaStreamTrackEvent('addtrack', { track })); }); // Dispatch an unmute event for the track. track._setMutedInternal(false); } log.debug(`${this._pcId} setRemoteDescription OK`); } async addIceCandidate(candidate): Promise<void> { log.debug(`${this._pcId} addIceCandidate`); if (!candidate || !candidate.candidate) { // XXX end-of candidates is not implemented: https://bugs.chromium.org/p/webrtc/issues/detail?id=9218 return; } if ( candidate.sdpMLineIndex === null || candidate.sdpMLineIndex === undefined || candidate.sdpMid === null || candidate.sdpMid === undefined ) { throw new TypeError('`sdpMLineIndex` and `sdpMid` must not null or undefined'); } const newSdp = await WebRTCModule.peerConnectionAddICECandidate( this._pcId, candidate.toJSON ? candidate.toJSON() : candidate ); this.remoteDescription = new RTCSessionDescription(newSdp); } /** * @brief Adds a new track to the {@link RTCPeerConnection}, * and indicates that it is contained in the specified {@link MediaStream}s. * This method has to be synchronous as the W3C API expects a track to be returned * @param {MediaStreamTrack} track The track to be added * @param {...MediaStream} streams One or more {@link MediaStream}s the track needs to be added to * https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtrack */ addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender { log.debug(`${this._pcId} addTrack`); if (this.connectionState === 'closed') { throw new Error('Peer Connection is closed'); } if (this._trackExists(track)) { throw new Error('Track already exists in a sender'); } const streamIds = streams.map(s => s.id); const result = WebRTCModule.peerConnectionAddTrack(this._pcId, track.id, { streamIds }); if (result === null) { throw new Error('Could not add sender'); } const { transceiverOrder, transceiver, sender } = result; // According to the W3C docs, the sender could have been reused, and // so we check if that is the case, and update accordingly. const [ existingSender ] = this .getSenders() .filter(s => s.id === sender.id); if (existingSender) { // Update sender existingSender._track = track; // Update the corresponding transceiver as well const [ existingTransceiver ] = this .getTransceivers() .filter(t => t.sender.id === existingSender.id); existingTransceiver._direction = transceiver.direction; existingTransceiver._currentDirection = transceiver.currentDirection; return existingSender; } // This is a new transceiver, should create a transceiver for it and add it const newSender = new RTCRtpSender({ ...transceiver.sender, track }); const remoteTrack = transceiver.receiver.track ? new MediaStreamTrack(transceiver.receiver.track) : null; const newReceiver = new RTCRtpReceiver({ ...transceiver.receiver, track: remoteTrack }); const newTransceiver = new RTCRtpTransceiver({ ...transceiver, sender: newSender, receiver: newReceiver, }); this._insertTransceiverSorted(transceiverOrder, newTransceiver); return newSender; } addTransceiver(source: 'audio' | 'video' | MediaStreamTrack, init): RTCRtpTransceiver { log.debug(`${this._pcId} addTransceiver`); let src = {}; if (source === 'audio') { src = { type: 'audio' }; } else if (source === 'video') { src = { type: 'video' }; } else { src = { trackId: source.id }; } // Extract the stream ids if (init && init.streams) { init.streamIds = init.streams.map(stream => stream.id); } const result = WebRTCModule.peerConnectionAddTransceiver(this._pcId, { ...src, init: { ...init } }); if (result === null) { throw new Error('Transceiver could not be added'); } const t = result.transceiver; let track: MediaStreamTrack | null = null; if (typeof source === 'string') { if (t.sender.track) { track = new MediaStreamTrack(t.sender.track); } } else { // 'source' is a MediaStreamTrack track = source; } const sender = new RTCRtpSender({ ...t.sender, track }); const remoteTrack = t.receiver.track ? new MediaStreamTrack(t.receiver.track) : null; const receiver = new RTCRtpReceiver({ ...t.receiver, track: remoteTrack }); const transceiver = new RTCRtpTransceiver({ ...result.transceiver, sender, receiver }); this._insertTransceiverSorted(result.transceiverOrder, transceiver); return transceiver; } removeTrack(sender: RTCRtpSender) { log.debug(`${this._pcId} removeTrack`); if (this._pcId !== sender._peerConnectionId) { throw new Error('Sender does not belong to this peer connection'); } if (this.connectionState === 'closed') { throw new Error('Peer Connection is closed'); } const existingSender = this .getSenders() .find(s => s === sender); if (!existingSender) { throw new Error('Sender does not exist'); } if (existingSender.track === null) { return; } // Blocking! WebRTCModule.peerConnectionRemoveTrack(this._pcId, sender.id); existingSender._track = null; const [ existingTransceiver ] = this .getTransceivers() .filter(t => t.sender.id === existingSender.id); existingTransceiver._direction = existingTransceiver.direction === 'sendrecv' ? 'recvonly' : 'inactive'; } async getStats(selector?: MediaStreamTrack) { log.debug(`${this._pcId} getStats`); if (!selector) { const data = await WebRTCModule.peerConnectionGetStats(this._pcId); /** * On both Android and iOS it is faster to construct a single * JSON string representing the Map of StatsReports and have it * pass through the React Native bridge rather than the Map of * StatsReports. While the implementations do try to be faster in * general, the stress is on being faster to pass through the React * Native bridge which is a bottleneck that tends to be visible in * the UI when there is congestion involving UI-related passing. */ return new Map(JSON.parse(data)); } else { const senders = this.getSenders().filter(s => s.track === selector); const receivers = this.getReceivers().filter(r => r.track === selector); const matches = senders.length + receivers.length; if (matches === 0) { throw new Error('Invalid selector: could not find matching sender / receiver'); } else if (matches > 1) { throw new Error('Invalid selector: multiple matching senders / receivers'); } else { const sr = senders[0] || receivers[0]; return sr.getStats(); } } } getTransceivers(): RTCRtpTransceiver[] { return this._transceivers.map(e => e.transceiver); } getSenders(): RTCRtpSender[] { // @ts-ignore return this._transceivers.map(e => !e.transceiver.stopped && e.transceiver.sender).filter(Boolean); } getReceivers(): RTCRtpReceiver[] { // @ts-ignore return this._transceivers.map(e => !e.transceiver.stopped && e.transceiver.receiver).filter(Boolean); } close(): void { log.debug(`${this._pcId} close`); if (this.connectionState === 'closed') { return; } WebRTCModule.peerConnectionClose(this._pcId); // Mark transceivers as stopped. this._transceivers.forEach(({ transceiver })=> { transceiver._setStopped(); }); } restartIce(): void { WebRTCModule.peerConnectionRestartIce(this._pcId); } _registerEvents(): void { addListener(this, 'peerConnectionOnRenegotiationNeeded', (ev: any) => { if (ev.pcId !== this._pcId) { return; } this.dispatchEvent(new Event('negotiationneeded')); }); addListener(this, 'peerConnectionIceConnectionChanged', (ev: any) => { if (ev.pcId !== this._pcId) { return; } this.iceConnectionState = ev.iceConnectionState; this.dispatchEvent(new Event('iceconnectionstatechange')); }); addListener(this, 'peerConnectionStateChanged', (ev: any) => { if (ev.pcId !== this._pcId) { return; } this.connectionState = ev.connectionState; this.dispatchEvent(new Event('connectionstatechange')); if (ev.connectionState === 'closed') { // This PeerConnection is done, clean up. removeListener(this); WebRTCModule.peerConnectionDispose(this._pcId); } }); addListener(this, 'peerConnectionSignalingStateChanged', (ev: any) => { if (ev.pcId !== this._pcId) { return; } this.signalingState = ev.signalingState; this.dispatchEvent(new Event('signalingstatechange')); }); // Consider moving away from this event: https://github.com/WebKit/WebKit/pull/3953 addListener(this, 'peerConnectionOnTrack', (ev: any) => { if (ev.pcId !== this._pcId) { return; } log.debug(`${this._pcId} ontrack`); // NOTE: We need to make sure the track event fires right before sRD completes, // so we queue them up here and dispatch the events when sRD fires, but before completing it. // In the future we should probably implement out own logic and drop this event altogether. this._pendingTrackEvents.push(ev); }); addListener(this, 'peerConnectionOnRemoveTrack', (ev: any) => { if (ev.pcId !== this._pcId) { return; } log.debug(`${this._pcId} onremovetrack ${ev.receiverId}`); const receiver = this.getReceivers().find(r => r.id === ev.receiverId); const track = receiver?.track; if (receiver && track) { // As per the spec: // - Remove the track from any media streams that were previously passed to the `track` event. // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-removetrack, // - Mark the track as muted: // https://w3c.github.io/webrtc-pc/#process-remote-track-removal for (const stream of this._remoteStreams.values()) { if (stream._tracks.includes(track)) { const trackIdx = stream._tracks.indexOf(track); log.debug(`${this._pcId} removetrack ${track.id}`); stream._tracks.splice(trackIdx, 1); stream.dispatchEvent(new MediaStreamTrackEvent('removetrack', { track })); // Dispatch a mute event for the track. track._setMutedInternal(true); } } } }); addListener(this, 'peerConnectionGotICECandidate', (ev: any) => { if (ev.pcId !== this._pcId) { return; } const sdpInfo = ev.sdp; // Can happen when doing a rollback. if (sdpInfo.type && sdpInfo.sdp) { this.localDescription = new RTCSessionDescription(sdpInfo); } else { this.localDescription = null; } const candidate = new RTCIceCandidate(ev.candidate); this.dispatchEvent(new RTCIceCandidateEvent('icecandidate', { candidate })); }); addListener(this, 'peerConnectionIceGatheringChanged', (ev: any) => { if (ev.pcId !== this._pcId) { return; } this.iceGatheringState = ev.iceGatheringState; if (this.iceGatheringState === 'complete') { const sdpInfo = ev.sdp; // Can happen when doing a rollback. if (sdpInfo.type && sdpInfo.sdp) { this.localDescription = new RTCSessionDescription(sdpInfo); } else { this.localDescription = null; } this.dispatchEvent(new RTCIceCandidateEvent('icecandidate', { candidate: null })); } this.dispatchEvent(new Event('icegatheringstatechange')); }); addListener(this, 'peerConnectionDidOpenDataChannel', (ev: any) => { if (ev.pcId !== this._pcId) { return; } const channel = new RTCDataChannel(ev.dataChannel); this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel })); // Send 'open' event. Native doesn't update the state since it's already // set at this point. channel.dispatchEvent(new RTCDataChannelEvent('open', { channel })); }); addListener(this, 'mediaStreamTrackMuteChanged', (ev: any) => { if (ev.pcId !== this._pcId) { return; } const [ track ] = this.getReceivers().map(r => r.track).filter(t => t?.id === ev.trackId); if (track) { track._setMutedInternal(ev.muted); } }); } /** * Creates a new RTCDataChannel object with the given label. The * RTCDataChannelInit dictionary can be used to configure properties of the * underlying channel such as data reliability. * * @param {string} label - the value with which the label attribute of the new * instance is to be initialized * @param {RTCDataChannelInit} dataChannelDict - an optional dictionary of * values with which to initialize corresponding attributes of the new * instance such as id */ createDataChannel(label: string, dataChannelDict?: RTCDataChannelInit): RTCDataChannel { if (dataChannelDict && 'id' in dataChannelDict) { const id = dataChannelDict.id; if (typeof id !== 'number') { throw new TypeError('DataChannel id must be a number: ' + id); } } const channelInfo = WebRTCModule.createDataChannel(this._pcId, label, dataChannelDict); if (channelInfo === null) { throw new TypeError('Failed to create new DataChannel'); } return new RTCDataChannel(channelInfo); } /** * Check whether a media stream track exists already in a sender. * See https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtrack for more information */ _trackExists(track: MediaStreamTrack): boolean { const [ sender ] = this .getSenders() .filter( sender => sender.track?.id === track.id ); return sender? true : false; } /** * Updates transceivers after offer/answer updates if necessary. */ _updateTransceivers(transceiverUpdates, removeStopped = false) { for (const update of transceiverUpdates) { const [ transceiver ] = this .getTransceivers() .filter(t => t.sender.id === update.transceiverId); if (!transceiver) { continue; } if (update.currentDirection) { transceiver._currentDirection = update.currentDirection; } transceiver._mid = update.mid; transceiver._stopped = Boolean(update.isStopped); transceiver._sender._rtpParameters = new RTCRtpSendParameters(update.senderRtpParameters); transceiver._receiver._rtpParameters = new RTCRtpReceiveParameters(update.receiverRtpParameters); } if (removeStopped) { const stopped = this.getTransceivers().filter(t => t.stopped); const newTransceivers = this._transceivers.filter(t => !stopped.includes(t.transceiver)); this._transceivers = newTransceivers; } } /** * Inserts transceiver into the transceiver array in the order they are created (timestamp). * @param order an index that refers to when it it was created relatively. * @param transceiver the transceiver object to be inserted. */ _insertTransceiverSorted(order: number, transceiver: RTCRtpTransceiver) { this._transceivers.push({ order, transceiver }); this._transceivers.sort((a, b) => a.order - b.order); } } /** * Define the `onxxx` event handlers. */ const proto = RTCPeerConnection.prototype; defineEventAttribute(proto, 'connectionstatechange'); defineEventAttribute(proto, 'icecandidate'); defineEventAttribute(proto, 'icecandidateerror'); defineEventAttribute(proto, 'iceconnectionstatechange'); defineEventAttribute(proto, 'icegatheringstatechange'); defineEventAttribute(proto, 'negotiationneeded'); defineEventAttribute(proto, 'signalingstatechange'); defineEventAttribute(proto, 'datachannel'); defineEventAttribute(proto, 'track'); defineEventAttribute(proto, 'error');